mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-17 18:35:19 +02:00
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
This commit is contained in:
parent
f2e38c52a1
commit
d20cb8a538
9 changed files with 979 additions and 125 deletions
|
|
@ -8,135 +8,66 @@ stepsCompleted:
|
|||
- 6
|
||||
- 7
|
||||
- 8
|
||||
- 9
|
||||
- 14
|
||||
inputDocuments:
|
||||
- _bmad-output/planning-artifacts/prd.md
|
||||
- _bmad-epics/epic-1-ai-powered-crypto-assistant.md
|
||||
- _bmad-epics/epic-2-smart-monitoring-alerts.md
|
||||
- _bmad-epics/epic-3-trading-intelligence.md
|
||||
- _bmad-epics/epic-4-content-creation-productivity.md
|
||||
---
|
||||
- _bmad-epics/epic-3-trading-execution-safety.md
|
||||
- _bmad-epics/epic-4-adaptive-learning-evolution.md
|
||||
outputDocument: _bmad-output/planning-artifacts/ux-design-specification.md
|
||||
|
||||
# UX Design Specification SurfSense
|
||||
# UX Design Specification: SurfSense v2
|
||||
|
||||
**Author:** Luis
|
||||
**Date:** 2026-02-02
|
||||
## 1. Design Strategy
|
||||
**"Evolution, Not Revolution"**
|
||||
- **Core Philosophy:** SurfSense v2 features will be "grafted" (injected) into the existing Research Dashboard rather than replacing it.
|
||||
- **Goal:** Maintain the professional, clean utility of the current "SaaS-style" research tool while adding "Crypto-Native" signals only where critical (the "Intel Layer").
|
||||
- **Visual Identity:** "Smart Cards" & "Traffic Lights" embedded in a Clean UI.
|
||||
|
||||
## 2. Core User Experience
|
||||
**The "Intel Layer" Concept**
|
||||
- **Problem:** Users have to switch contexts between Charts (DexScreener) and Safety Scans (Scanners/Twitter).
|
||||
- **Solution:** A hybrid "Intel Layer" that sits *on top* of the chart or *inside* the research chat.
|
||||
- **Mental Model:** "Traffic Light System" (Red/Green/Yellow).
|
||||
- 🔴 **Stop:** Scam/Rug/High Risk. (Immediate Action: Ignore)
|
||||
- 🟢 **Go:** Safe/Whale Buying. (Immediate Action: Ape in/Research)
|
||||
- 🟡 **Caution:** Mixed signals. (Action: Open Dashboard for Deep Dive)
|
||||
|
||||
**Key Mechanics:**
|
||||
1. **Initiation:** Extension detects Token URL -> Shows Traffic Light Badge.
|
||||
2. **Interaction:** Click Badge -> Opens Overlay (Summary).
|
||||
3. **Deep Dive:** "Ask SurfSense" -> Redirects to existing Web Dashboard (Port 3999).
|
||||
4. **Integration:** Inside the Chat Interface, AI answers are formatted as **Rich UI Cards**, not just text.
|
||||
|
||||
## 3. Visual Foundation (Confirmed Step 8 & 9)
|
||||
**Tokens & Theming**
|
||||
- **Source of Truth:** Inherited from existing `surfsense_web` (`globals.css` + Tailwind v4).
|
||||
- **Base Theme:** Dark Mode (Deep Navy / `#0B1221`) to match crypto aesthetic but cleaner.
|
||||
- **Typography:** Inter (Sans) for UI, Geist Mono for Data/Numbers.
|
||||
|
||||
**Color Palette Extension (The "Traffic Light" Overlay)**
|
||||
* **Success (Buy/Safe):** `#22c55e` (Green-500) - Solid, trustworthy.
|
||||
* **Danger (Scam/Sell):** `#f43f5e` (Rose-500) - Urgent, alarming.
|
||||
* **Warning (Volatility):** `#f59e0b` (Amber-500) - Cautionary.
|
||||
* **Whale (Institutional):** `#3b82f6` (Blue-500) - Consistent with brand.
|
||||
|
||||
## 4. Component Strategy (Smart Cards)
|
||||
Instead of building a full trading terminal, we build **"Injectable Components"**:
|
||||
1. **Signal Card:** Used in Chat & Extension. Shows Risk Score + 3 Key Bullets.
|
||||
2. **Whale Alert Row:** Used in Lists & Notifications. Shows "Wallet X bought $50K".
|
||||
3. **Mini-Chart:** Sparklines only, for quick trend context.
|
||||
|
||||
## 5. Mobile & Responsive
|
||||
- **Extension:** Fixed width (360px-400px), focused on "At-a-glance" data.
|
||||
- **Web Dashboard:** Responsive, but optimized for Desktop Research. Mobile view converts Tables to Cards.
|
||||
|
||||
## 6. Implementation Notes
|
||||
- **Frontend:** Next.js (Port 3999). Use standard Tailwind classes.
|
||||
- **Backend:** FastAPI (Port 3998). Serves structured JSON for UI Cards.
|
||||
- **Extension:** Chrome Side Panel API. Shares UI components with Web via Repo Monorepo/Shared Lib structure.
|
||||
|
||||
---
|
||||
|
||||
<!-- UX design content will be appended sequentially through collaborative workflow steps -->
|
||||
|
||||
## Executive Summary
|
||||
|
||||
### Project Vision
|
||||
SurfSense 2.0 transforms from a general-purpose tool into a **specialized AI Co-pilot for Crypto Traders**. The core value proposition shifts from passive data aggregation to **proactive intelligence**—providing "Smart Monitoring," "Trading Intelligence," and "Content Creation" tools that work seamlessly alongside the trader's workflow.
|
||||
|
||||
### Target Users
|
||||
* **Momentum Traders:** Need real-time, "hot" information (Whale alerts, Volume spikes) to catch rapid price movements. They prioritize speed and accessibility (Extension).
|
||||
* **Cautious Investors:** Prioritize safety and due diligence. They need tools to verify contracts, detect rug pulls, and analyze long-term metrics.
|
||||
* **Content Creators:** Use the platform to generate insights and share them (charts, threads) to build their audience.
|
||||
|
||||
### Key Design Challenges (Web-First)
|
||||
* **Data Density vs. Clarity:** The new features (Portfolio, Market Intelligence) introduce complex data (charts, tables, metrics) that must be displayed without overwhelming the user, distinguishing this from the chat-heavy v1.
|
||||
* **Navigation Scalability:** The current chat-centric sidebar is insufficient for a multi-module application. We must integrate new functional areas (Market, Portfolio, Alerts) without burying them or cluttering the interface.
|
||||
* **Hybrid Workflow:** Users will constantly switch between "Deep Dive" analysis on the Web Dashboard and "Quick Checks" via the Extension. The experience must be consistent and synchronized.
|
||||
|
||||
### Design Opportunities
|
||||
* **Hybrid Interface Structure:** Transitioning the Web Dashboard from a purely "Chat UI" to a **"Hybrid Interface"** that balances **App Modules** (for data/tools) with the **AI Assistant** (for query/support). This allows distinct spaces for "doing" (Trading/Monitoring) and "asking" (Chat).
|
||||
* **Unified Design System:** Leveraging the existing Web Design System (Tailwind/Shadcn) to rapidly build the Extension UI, ensuring a consistent look and feel while reducing development effort ($18K constraint).
|
||||
|
||||
## Core User Experience
|
||||
|
||||
### Defining Experience: "The Intel Layer"
|
||||
SurfSense is not where users go to *see* price (they use DexScreener for that), but where they go to *see* **The Truth**. The defining interaction is an **"Instant Reality Check"**: while the chart shows hype (FOMO), SurfSense overlays the cold, hard data (Risk/Whale movement), allowing users to verify a trade in seconds. It acts as the "Verify" step in the "Detect → Verify → Act" loop.
|
||||
|
||||
### User Mental Model
|
||||
* **The Old Way:** See price spike → Check Twitter (hype) → Search Contract (manual) → Check Holders → Panic/FOMO → Buy blindly.
|
||||
* **The SurfSense Way:** See price spike → **Glance at Extension (Traffic Light)**:
|
||||
* 🔴 **Red:** Ignore immediately (Rug/Honeypot). Time saved: 10 mins.
|
||||
* 🟢 **Green:** Trade with confidence.
|
||||
* 🟡 **Yellow:** "Investigate" → One click to open Web Dashboard for deep reasoning (Whale behavior, Fresh wallet movement).
|
||||
|
||||
### Platform Strategy
|
||||
* **Web Dashboard (Master):** The "Command Center" for Portfolio, Alert Management, and Deep Intelligence Analysis. Focuses on **textual/numerical insights** over graphical charts.
|
||||
* **Extension (Satellite):** The "Tactical" tool for instant context. Smartly advises the user based on their current active tab using the **"Symbiotic Side-Panel"** pattern (lives alongside the chart, doesn't block it).
|
||||
|
||||
### Experience Mechanics
|
||||
1. **Initiation:** User navigates to a token on DexScreener/Twitter.
|
||||
2. **Interaction:** Extension Badge updates color (Red/Green). User clicks for Summary Overlay.
|
||||
3. **Cross-Over:** User clicks "Open Dashboard" for deep dive (if needed). Web App opens to the exact token context.
|
||||
4. **Completion:** User executes trade on DexScreener (or bot) with full confidence.
|
||||
|
||||
### Success Criteria
|
||||
* **Time-to-Truth < 5s:** User determines safety (SCAM vs LEGIT) within 5 seconds of landing on a chart.
|
||||
* **"Savior" Frequency:** User experiences a "saved me from a rug" moment at least once per week.
|
||||
* **Zero-Context Switching:** User never manually copies a contract address; the system auto-detects context.
|
||||
|
||||
### Novel UX Patterns
|
||||
* **"Evidence-First" AI:** Insights are always coupled with proof (e.g., "Bullish *because* 3 whales bought" with links to txns), avoiding "Black Box" trust issues.
|
||||
* **Traffic Light Risk Coding:** Universal color cues (Green=Safe, Yellow=Caution, Red=Danger) for Risk Scores allow scanning in < 1 second.
|
||||
|
||||
## Desired Emotional Response
|
||||
|
||||
### Primary Emotional Goals
|
||||
* **Confidence (Tự tin):** Users feel they possess "Insider Intelligence" that others missing by relying solely on charts. The AI provides the "Why" behind the price action.
|
||||
* **Calm (Bình tĩnh):** In a chaotic market (FOMO, rapid candles), SurfSense acts as a stabilizing anchor, providing "Cold Hard Data" (Risk Scores, On-chain metrics) to rationalize decision-making.
|
||||
* **Clarity (Sự rõ ràng):** Cutting through the noise of social media and complex charts to show the simple truth about a token's safety and potential.
|
||||
|
||||
### Emotional Journey Mapping
|
||||
* **Trigger (Alert):** **Urgency & Curiosity.** "Whale Accumulating" alert sparks immediate interest but balanced with a need to know *why*.
|
||||
* **Action (Verify):** **Reassurance.** Opening the dashboard confirms safety (Verified Contract) and validates the trend (AI Sentiment). Confusion turns into clarity.
|
||||
* **Result (Decision):** **Superiority & Relief.** User feels smarter than the herd ("I avoided a rug pull" or "I caught a trend early").
|
||||
|
||||
## UX Pattern Analysis & Inspiration
|
||||
|
||||
### Inspiring Products Analysis
|
||||
* **DexScreener (Crypto):** Excellent **Data Density**. They maximize screen real estate to show price, txns, and liquidity simultaneously without clutter. *Inspiration: High-density layouts using color coding (Green/Red) to direct attention.*
|
||||
* **GMGN.ai (Crypto):** Strong **Risk Visualization**. They surface hidden risks (dev dumping, high holder concentration) prominently. *Inspiration: "Warning Badges" and "Risk Clusters" that are impossible to ignore.*
|
||||
* **Perplexity AI (Non-Crypto):** Mastering **Trust & Citations**. Every AI claim is backed by a source link. *Inspiration: SurfSense AI insights should link back to source data (e.g., "Whale bought" -> Link to Txn).*
|
||||
* **Linear (Productivity):** **Keyboard-First Navigation** and speed. *Inspiration: Power user shortcuts (Cmd+K) for quick search and navigation between modules.*
|
||||
|
||||
### Key Takeaways
|
||||
* **Terminal-Style Efficiency:** Use a dense, tabular view for the "Market Intelligence" module (Web Dashboard) to allow sorting/filtering of 50+ tokens instantly.
|
||||
* **No Chart, Just Intel:** Don't replicate DexScreener. Provide the "Why" (Insights) behind the "What" (Price).
|
||||
|
||||
## Design System Foundation
|
||||
|
||||
### 1.1 Design System Choice
|
||||
**Shadcn/UI + Tailwind CSS** (Confirmed Existing Stack).
|
||||
|
||||
### Rationale for Selection
|
||||
* **Consistency:** The existing `surfsense_web` frontend already utilizes Tailwind CSS (v4) and Shadcn/UI components (Radix primitives). Maintaining this stack ensures zero friction between the current codebase and new v2 features.
|
||||
* **Inheritance:** The Extension (Slave) will directly inherit color tokens and typography from the Web Dashboard's `tailwind.config.js`, ensuring a unified brand experience with minimal effort.
|
||||
|
||||
### Implementation Approach
|
||||
* **Web-First Truth:** The Web Dashboard remains the "Master" for design tokens and component definitions.
|
||||
* **Dark Mode Native:** The system is already optimized for Dark Mode, which aligns perfectly with the crypto trading persona.
|
||||
* **Customization:** Extend default Shadcn theme with "Signal Colors" (Neon Green, Alert Red) for financial data visualization.
|
||||
|
||||
## Visual Design Foundation
|
||||
|
||||
### Color System
|
||||
* **Core Palette (Inherited):** Maintain established `globals.css` structure (OKLCH variables) for 100% implementation speed.
|
||||
* Background: `oklch(0.145 0 0)` (Dark Gray) for enterprise-grade stability.
|
||||
* Foreground: `oklch(0.985 0 0)` (White).
|
||||
* **Signal Colors (New):** High-saturation "Neon" variants for "Traffic Light" indicators in Dark Mode.
|
||||
* 🟢 **Success:** `oklch(0.6 0.18 145)` (Neon Green) - Use for "Safe", "Verified", "Buy Signal".
|
||||
* 🔴 **Danger:** `oklch(0.6 0.2 25)` (Neon Red) - Use for "Scam", "Rug Risk", "Sell Signal".
|
||||
* 🟡 **Warning:** `oklch(0.8 0.15 85)` (Amber) - Use for "Caution", "Low Liquidity".
|
||||
|
||||
### Typography System
|
||||
* **Primary Font:** **Geist Sans** (Inherited).
|
||||
* *Rationale:* Optimized for Vercel/Next.js stack, zero layout shift, and includes excellent **tabular figures** for price data.
|
||||
* *Usage:* All UI text, headers, and especially data tables.
|
||||
* **Tone:** Professional, direct, data-first. No decorative serifs.
|
||||
|
||||
### Spacing & Layout Foundation
|
||||
* **Base Unit:** `0.25rem` (4px). Standard Tailwind grid.
|
||||
* **Radius:** `0.625rem` (Default) for cards/inputs to match existing Web UI.
|
||||
* **Density Strategy:**
|
||||
* **Extension:** "Standard" density for touch/click friendliness.
|
||||
* **Market Intelligence (Web):** "Compact" density to maximize rows per screen (Terminal feel).
|
||||
|
||||
### Accessibility Considerations
|
||||
* **High Contrast Signals:** Prioritize distinctive colors for status indicators (Red/Green) but ensure they are accompanied by text/icon labels (not color-only) to support color-blind users.
|
||||
* **Dark Mode Optimization:** Ensure text contrast remains high (AA standard) against the dark gray background.
|
||||
**Status:** ✅ APPROVED
|
||||
**Next Steps:** Proceed to Architecture Design to map these UI components to Backend APIs.
|
||||
|
|
|
|||
|
|
@ -13,10 +13,16 @@ Available tools:
|
|||
- scrape_webpage: Extract content from webpages
|
||||
- save_memory: Store facts/preferences about the user
|
||||
- recall_memory: Retrieve relevant user memories
|
||||
- get_live_token_price: Get real-time crypto price from DexScreener
|
||||
- get_live_token_data: Get comprehensive real-time crypto market data
|
||||
"""
|
||||
|
||||
# Registry exports
|
||||
# Tool factory exports (for direct use)
|
||||
from .crypto_realtime import (
|
||||
create_get_live_token_data_tool,
|
||||
create_get_live_token_price_tool,
|
||||
)
|
||||
from .display_image import create_display_image_tool
|
||||
from .knowledge_base import (
|
||||
CONNECTOR_DESCRIPTIONS,
|
||||
|
|
@ -48,6 +54,8 @@ __all__ = [
|
|||
# Tool factories
|
||||
"create_display_image_tool",
|
||||
"create_generate_podcast_tool",
|
||||
"create_get_live_token_data_tool",
|
||||
"create_get_live_token_price_tool",
|
||||
"create_link_preview_tool",
|
||||
"create_recall_memory_tool",
|
||||
"create_save_memory_tool",
|
||||
|
|
|
|||
322
surfsense_backend/app/agents/new_chat/tools/crypto_realtime.py
Normal file
322
surfsense_backend/app/agents/new_chat/tools/crypto_realtime.py
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
"""
|
||||
Real-time cryptocurrency data tools for the SurfSense agent.
|
||||
|
||||
This module provides tools for fetching LIVE crypto data directly from DexScreener API.
|
||||
These tools complement the RAG-based search_knowledge_base tool:
|
||||
- RAG (search_knowledge_base): Historical context, trends, analysis from indexed data
|
||||
- Real-time tools: Current prices, live market data
|
||||
|
||||
The AI agent decides which to use based on the query:
|
||||
- "What's the current price of BULLA?" → get_live_token_price (real-time)
|
||||
- "How has BULLA performed this week?" → search_knowledge_base (RAG)
|
||||
- "Analyze BULLA for me" → Both (RAG for context + real-time for current data)
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.tools import tool
|
||||
|
||||
from app.connectors.dexscreener_connector import DexScreenerConnector
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def generate_token_id(chain: str, address: str) -> str:
|
||||
"""Generate a unique ID for a token query."""
|
||||
hash_val = hashlib.md5(f"{chain}:{address}".encode()).hexdigest()[:12]
|
||||
return f"token-{hash_val}"
|
||||
|
||||
|
||||
def create_get_live_token_price_tool():
|
||||
"""
|
||||
Factory function to create the get_live_token_price tool.
|
||||
|
||||
This tool fetches REAL-TIME price data directly from DexScreener API.
|
||||
Use this when users ask for current/live prices.
|
||||
|
||||
Returns:
|
||||
A configured tool function for fetching live token prices.
|
||||
"""
|
||||
|
||||
@tool
|
||||
async def get_live_token_price(
|
||||
chain: str,
|
||||
token_address: str,
|
||||
token_symbol: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Get the LIVE/CURRENT price of a cryptocurrency token from DexScreener.
|
||||
|
||||
Use this tool when the user asks for:
|
||||
- Current price: "What's the price of BULLA right now?"
|
||||
- Live data: "Show me live price for SOL"
|
||||
- Real-time info: "What's WETH trading at?"
|
||||
|
||||
DO NOT use this for historical analysis - use search_knowledge_base instead.
|
||||
|
||||
Args:
|
||||
chain: Blockchain network (e.g., 'solana', 'ethereum', 'base', 'bsc')
|
||||
token_address: The token's contract address
|
||||
token_symbol: Optional token symbol for display (e.g., 'BULLA', 'SOL')
|
||||
|
||||
Returns:
|
||||
Dictionary with live price data including:
|
||||
- price_usd: Current price in USD
|
||||
- price_change_24h: 24-hour price change percentage
|
||||
- price_change_1h: 1-hour price change percentage
|
||||
- volume_24h: 24-hour trading volume
|
||||
- liquidity_usd: Total liquidity in USD
|
||||
- market_cap: Market capitalization
|
||||
- dex: DEX where the best liquidity is found
|
||||
- pair_url: Link to DexScreener chart
|
||||
"""
|
||||
token_id = generate_token_id(chain, token_address)
|
||||
|
||||
try:
|
||||
# Initialize DexScreener connector
|
||||
connector = DexScreenerConnector()
|
||||
|
||||
# Fetch live data from API
|
||||
pairs, error = await connector.get_token_pairs(chain, token_address)
|
||||
|
||||
if error:
|
||||
logger.warning(f"[get_live_token_price] Error: {error}")
|
||||
return {
|
||||
"id": token_id,
|
||||
"kind": "live_token_price",
|
||||
"chain": chain,
|
||||
"token_address": token_address,
|
||||
"token_symbol": token_symbol,
|
||||
"error": error,
|
||||
}
|
||||
|
||||
if not pairs:
|
||||
return {
|
||||
"id": token_id,
|
||||
"kind": "live_token_price",
|
||||
"chain": chain,
|
||||
"token_address": token_address,
|
||||
"token_symbol": token_symbol,
|
||||
"error": f"No trading pairs found for {token_symbol or token_address} on {chain}",
|
||||
}
|
||||
|
||||
# Get the best pair (highest liquidity)
|
||||
best_pair = max(pairs, key=lambda p: float(p.get("liquidity", {}).get("usd", 0) or 0))
|
||||
|
||||
# Extract data from best pair
|
||||
base_token = best_pair.get("baseToken", {})
|
||||
price_change = best_pair.get("priceChange", {})
|
||||
volume = best_pair.get("volume", {})
|
||||
liquidity = best_pair.get("liquidity", {})
|
||||
|
||||
return {
|
||||
"id": token_id,
|
||||
"kind": "live_token_price",
|
||||
"chain": chain,
|
||||
"token_address": token_address,
|
||||
"token_symbol": token_symbol or base_token.get("symbol", "Unknown"),
|
||||
"token_name": base_token.get("name", "Unknown"),
|
||||
"price_usd": best_pair.get("priceUsd", "N/A"),
|
||||
"price_native": best_pair.get("priceNative", "N/A"),
|
||||
"price_change_5m": price_change.get("m5", 0),
|
||||
"price_change_1h": price_change.get("h1", 0),
|
||||
"price_change_6h": price_change.get("h6", 0),
|
||||
"price_change_24h": price_change.get("h24", 0),
|
||||
"volume_24h": volume.get("h24", 0),
|
||||
"volume_6h": volume.get("h6", 0),
|
||||
"volume_1h": volume.get("h1", 0),
|
||||
"liquidity_usd": liquidity.get("usd", 0),
|
||||
"market_cap": best_pair.get("marketCap", 0),
|
||||
"fdv": best_pair.get("fdv", 0),
|
||||
"dex": best_pair.get("dexId", "Unknown"),
|
||||
"pair_address": best_pair.get("pairAddress", ""),
|
||||
"pair_url": best_pair.get("url", ""),
|
||||
"total_pairs": len(pairs),
|
||||
"data_source": "DexScreener API (Real-time)",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
logger.error(f"[get_live_token_price] Error fetching {chain}/{token_address}: {error_message}")
|
||||
return {
|
||||
"id": token_id,
|
||||
"kind": "live_token_price",
|
||||
"chain": chain,
|
||||
"token_address": token_address,
|
||||
"token_symbol": token_symbol,
|
||||
"error": f"Failed to fetch live price: {error_message[:100]}",
|
||||
}
|
||||
|
||||
return get_live_token_price
|
||||
|
||||
|
||||
def create_get_live_token_data_tool():
|
||||
"""
|
||||
Factory function to create the get_live_token_data tool.
|
||||
|
||||
This tool fetches comprehensive REAL-TIME market data from DexScreener API.
|
||||
Use this when users want detailed current market information.
|
||||
|
||||
Returns:
|
||||
A configured tool function for fetching live token market data.
|
||||
"""
|
||||
|
||||
@tool
|
||||
async def get_live_token_data(
|
||||
chain: str,
|
||||
token_address: str,
|
||||
token_symbol: str | None = None,
|
||||
include_all_pairs: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Get comprehensive LIVE market data for a cryptocurrency token.
|
||||
|
||||
Use this tool when the user asks for:
|
||||
- Detailed market info: "Show me full market data for BULLA"
|
||||
- Trading activity: "What's the trading volume for SOL?"
|
||||
- Liquidity info: "How much liquidity does WETH have?"
|
||||
- Transaction counts: "How many buys/sells for this token?"
|
||||
|
||||
This returns more detailed data than get_live_token_price.
|
||||
For historical trends and analysis, use search_knowledge_base instead.
|
||||
|
||||
Args:
|
||||
chain: Blockchain network (e.g., 'solana', 'ethereum', 'base', 'bsc')
|
||||
token_address: The token's contract address
|
||||
token_symbol: Optional token symbol for display
|
||||
include_all_pairs: If True, include data from all trading pairs
|
||||
|
||||
Returns:
|
||||
Dictionary with comprehensive market data including:
|
||||
- All price data from get_live_token_price
|
||||
- Transaction counts (buys/sells in 24h, 6h, 1h)
|
||||
- All trading pairs (if include_all_pairs=True)
|
||||
- Aggregated volume across all pairs
|
||||
"""
|
||||
token_id = generate_token_id(chain, token_address)
|
||||
|
||||
try:
|
||||
# Initialize DexScreener connector
|
||||
connector = DexScreenerConnector()
|
||||
|
||||
# Fetch live data from API
|
||||
pairs, error = await connector.get_token_pairs(chain, token_address)
|
||||
|
||||
if error:
|
||||
logger.warning(f"[get_live_token_data] Error: {error}")
|
||||
return {
|
||||
"id": token_id,
|
||||
"kind": "live_token_data",
|
||||
"chain": chain,
|
||||
"token_address": token_address,
|
||||
"token_symbol": token_symbol,
|
||||
"error": error,
|
||||
}
|
||||
|
||||
if not pairs:
|
||||
return {
|
||||
"id": token_id,
|
||||
"kind": "live_token_data",
|
||||
"chain": chain,
|
||||
"token_address": token_address,
|
||||
"token_symbol": token_symbol,
|
||||
"error": f"No trading pairs found for {token_symbol or token_address} on {chain}",
|
||||
}
|
||||
|
||||
# Get the best pair (highest liquidity)
|
||||
best_pair = max(pairs, key=lambda p: float(p.get("liquidity", {}).get("usd", 0) or 0))
|
||||
|
||||
# Extract data from best pair
|
||||
base_token = best_pair.get("baseToken", {})
|
||||
price_change = best_pair.get("priceChange", {})
|
||||
volume = best_pair.get("volume", {})
|
||||
liquidity = best_pair.get("liquidity", {})
|
||||
txns = best_pair.get("txns", {})
|
||||
|
||||
# Calculate aggregated stats across all pairs
|
||||
total_volume_24h = sum(float(p.get("volume", {}).get("h24", 0) or 0) for p in pairs)
|
||||
total_liquidity = sum(float(p.get("liquidity", {}).get("usd", 0) or 0) for p in pairs)
|
||||
total_buys_24h = sum(p.get("txns", {}).get("h24", {}).get("buys", 0) or 0 for p in pairs)
|
||||
total_sells_24h = sum(p.get("txns", {}).get("h24", {}).get("sells", 0) or 0 for p in pairs)
|
||||
|
||||
result = {
|
||||
"id": token_id,
|
||||
"kind": "live_token_data",
|
||||
"chain": chain,
|
||||
"token_address": token_address,
|
||||
"token_symbol": token_symbol or base_token.get("symbol", "Unknown"),
|
||||
"token_name": base_token.get("name", "Unknown"),
|
||||
# Price data
|
||||
"price_usd": best_pair.get("priceUsd", "N/A"),
|
||||
"price_native": best_pair.get("priceNative", "N/A"),
|
||||
"price_change_5m": price_change.get("m5", 0),
|
||||
"price_change_1h": price_change.get("h1", 0),
|
||||
"price_change_6h": price_change.get("h6", 0),
|
||||
"price_change_24h": price_change.get("h24", 0),
|
||||
# Volume data (best pair)
|
||||
"volume_24h": volume.get("h24", 0),
|
||||
"volume_6h": volume.get("h6", 0),
|
||||
"volume_1h": volume.get("h1", 0),
|
||||
"volume_5m": volume.get("m5", 0),
|
||||
# Liquidity
|
||||
"liquidity_usd": liquidity.get("usd", 0),
|
||||
"liquidity_base": liquidity.get("base", 0),
|
||||
"liquidity_quote": liquidity.get("quote", 0),
|
||||
# Market metrics
|
||||
"market_cap": best_pair.get("marketCap", 0),
|
||||
"fdv": best_pair.get("fdv", 0),
|
||||
# Transaction counts (best pair)
|
||||
"txns_24h_buys": txns.get("h24", {}).get("buys", 0),
|
||||
"txns_24h_sells": txns.get("h24", {}).get("sells", 0),
|
||||
"txns_6h_buys": txns.get("h6", {}).get("buys", 0),
|
||||
"txns_6h_sells": txns.get("h6", {}).get("sells", 0),
|
||||
"txns_1h_buys": txns.get("h1", {}).get("buys", 0),
|
||||
"txns_1h_sells": txns.get("h1", {}).get("sells", 0),
|
||||
# Aggregated stats (all pairs)
|
||||
"total_volume_24h_all_pairs": total_volume_24h,
|
||||
"total_liquidity_all_pairs": total_liquidity,
|
||||
"total_buys_24h_all_pairs": total_buys_24h,
|
||||
"total_sells_24h_all_pairs": total_sells_24h,
|
||||
# DEX info
|
||||
"dex": best_pair.get("dexId", "Unknown"),
|
||||
"pair_address": best_pair.get("pairAddress", ""),
|
||||
"pair_url": best_pair.get("url", ""),
|
||||
"pair_created_at": best_pair.get("pairCreatedAt"),
|
||||
# Metadata
|
||||
"total_pairs": len(pairs),
|
||||
"data_source": "DexScreener API (Real-time)",
|
||||
}
|
||||
|
||||
# Include all pairs if requested
|
||||
if include_all_pairs and len(pairs) > 1:
|
||||
result["all_pairs"] = [
|
||||
{
|
||||
"dex": p.get("dexId"),
|
||||
"pair_address": p.get("pairAddress"),
|
||||
"quote_symbol": p.get("quoteToken", {}).get("symbol"),
|
||||
"price_usd": p.get("priceUsd"),
|
||||
"liquidity_usd": p.get("liquidity", {}).get("usd", 0),
|
||||
"volume_24h": p.get("volume", {}).get("h24", 0),
|
||||
"url": p.get("url"),
|
||||
}
|
||||
for p in sorted(pairs, key=lambda x: float(x.get("liquidity", {}).get("usd", 0) or 0), reverse=True)[:10]
|
||||
]
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
logger.error(f"[get_live_token_data] Error fetching {chain}/{token_address}: {error_message}")
|
||||
return {
|
||||
"id": token_id,
|
||||
"kind": "live_token_data",
|
||||
"chain": chain,
|
||||
"token_address": token_address,
|
||||
"token_symbol": token_symbol,
|
||||
"error": f"Failed to fetch live data: {error_message[:100]}",
|
||||
}
|
||||
|
||||
return get_live_token_data
|
||||
|
||||
|
|
@ -43,6 +43,10 @@ from typing import Any
|
|||
|
||||
from langchain_core.tools import BaseTool
|
||||
|
||||
from .crypto_realtime import (
|
||||
create_get_live_token_data_tool,
|
||||
create_get_live_token_price_tool,
|
||||
)
|
||||
from .display_image import create_display_image_tool
|
||||
from .knowledge_base import create_search_knowledge_base_tool
|
||||
from .link_preview import create_link_preview_tool
|
||||
|
|
@ -179,6 +183,26 @@ BUILTIN_TOOLS: list[ToolDefinition] = [
|
|||
# factory=lambda deps: create_my_custom_tool(...),
|
||||
# requires=["search_space_id"],
|
||||
# ),
|
||||
# =========================================================================
|
||||
# CRYPTO REAL-TIME TOOLS - Hybrid approach (RAG + Real-time)
|
||||
# =========================================================================
|
||||
# These tools fetch LIVE data directly from DexScreener API.
|
||||
# Use alongside search_knowledge_base for comprehensive crypto analysis:
|
||||
# - search_knowledge_base: Historical context, trends (from indexed data)
|
||||
# - get_live_token_price: Current price (real-time API call)
|
||||
# - get_live_token_data: Full market data (real-time API call)
|
||||
ToolDefinition(
|
||||
name="get_live_token_price",
|
||||
description="Get LIVE/CURRENT cryptocurrency price from DexScreener API. Use for real-time price queries.",
|
||||
factory=lambda deps: create_get_live_token_price_tool(),
|
||||
requires=[],
|
||||
),
|
||||
ToolDefinition(
|
||||
name="get_live_token_data",
|
||||
description="Get comprehensive LIVE market data (price, volume, liquidity, transactions) from DexScreener API.",
|
||||
factory=lambda deps: create_get_live_token_data_tool(),
|
||||
requires=[],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,23 @@ import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast";
|
|||
import { LinkPreviewToolUI } from "@/components/tool-ui/link-preview";
|
||||
import { ScrapeWebpageToolUI } from "@/components/tool-ui/scrape-webpage";
|
||||
import { RecallMemoryToolUI, SaveMemoryToolUI } from "@/components/tool-ui/user-memory";
|
||||
// Crypto Tool UI Components - Conversational Crypto Advisor
|
||||
import {
|
||||
TokenAnalysisToolUI,
|
||||
WatchlistDisplayToolUI,
|
||||
ActionConfirmationToolUI,
|
||||
AlertConfigurationToolUI,
|
||||
ProactiveAlertToolUI,
|
||||
TrendingTokensToolUI,
|
||||
WhaleActivityToolUI,
|
||||
MarketOverviewToolUI,
|
||||
HolderAnalysisToolUI,
|
||||
PortfolioDisplayToolUI,
|
||||
UserProfileToolUI,
|
||||
// Real-time crypto tools (Hybrid approach: RAG + Real-time)
|
||||
LiveTokenPriceToolUI,
|
||||
LiveTokenDataToolUI,
|
||||
} from "@/components/tool-ui/crypto";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { useChatSessionStateSync } from "@/hooks/use-chat-session-state";
|
||||
import { useMessagesElectric } from "@/hooks/use-messages-electric";
|
||||
|
|
@ -1458,6 +1475,21 @@ export default function NewChatPage() {
|
|||
<ScrapeWebpageToolUI />
|
||||
<SaveMemoryToolUI />
|
||||
<RecallMemoryToolUI />
|
||||
{/* Crypto Tool UI Components - Conversational Crypto Advisor */}
|
||||
<TokenAnalysisToolUI />
|
||||
<WatchlistDisplayToolUI />
|
||||
<ActionConfirmationToolUI />
|
||||
<AlertConfigurationToolUI />
|
||||
<ProactiveAlertToolUI />
|
||||
<TrendingTokensToolUI />
|
||||
<WhaleActivityToolUI />
|
||||
<MarketOverviewToolUI />
|
||||
<HolderAnalysisToolUI />
|
||||
<PortfolioDisplayToolUI />
|
||||
<UserProfileToolUI />
|
||||
{/* Real-time Crypto Tools - Hybrid approach (RAG + Real-time) */}
|
||||
<LiveTokenPriceToolUI />
|
||||
<LiveTokenDataToolUI />
|
||||
{/* <WriteTodosToolUI /> Disabled for now */}
|
||||
<div className="flex flex-col h-[calc(100vh-64px)] overflow-hidden">
|
||||
<Thread
|
||||
|
|
|
|||
130
surfsense_web/components/tool-ui/crypto/index.ts
Normal file
130
surfsense_web/components/tool-ui/crypto/index.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
/**
|
||||
* Crypto Tool UI Components
|
||||
*
|
||||
* These components render rich UI for crypto-related AI tools in the chat interface.
|
||||
* They follow the conversational UX paradigm where all crypto features are
|
||||
* AI-callable tools that render inline in the chat.
|
||||
*/
|
||||
|
||||
// Token Analysis - displays comprehensive token analysis
|
||||
export {
|
||||
TokenAnalysisToolUI,
|
||||
TokenAnalysisArgsSchema,
|
||||
TokenAnalysisResultSchema,
|
||||
type TokenAnalysisArgs,
|
||||
type TokenAnalysisResult,
|
||||
} from "./token-analysis";
|
||||
|
||||
// Watchlist Display - shows user's watchlist inline
|
||||
export {
|
||||
WatchlistDisplayToolUI,
|
||||
WatchlistDisplayArgsSchema,
|
||||
WatchlistDisplayResultSchema,
|
||||
type WatchlistDisplayArgs,
|
||||
type WatchlistDisplayResult,
|
||||
} from "./watchlist-display";
|
||||
|
||||
// Action Confirmation - confirms executed actions
|
||||
export {
|
||||
ActionConfirmationToolUI,
|
||||
ActionConfirmationArgsSchema,
|
||||
ActionConfirmationResultSchema,
|
||||
type ActionConfirmationArgs,
|
||||
type ActionConfirmationResult,
|
||||
} from "./action-confirmation";
|
||||
|
||||
// Alert Configuration - displays/edits alert settings
|
||||
export {
|
||||
AlertConfigurationToolUI,
|
||||
AlertConfigurationArgsSchema,
|
||||
AlertConfigurationResultSchema,
|
||||
type AlertConfigurationArgs,
|
||||
type AlertConfigurationResult,
|
||||
} from "./alert-configuration";
|
||||
|
||||
// Proactive Alert - AI-initiated alerts
|
||||
export {
|
||||
ProactiveAlertToolUI,
|
||||
ProactiveAlertArgsSchema,
|
||||
ProactiveAlertResultSchema,
|
||||
type ProactiveAlertArgs,
|
||||
type ProactiveAlertResult,
|
||||
} from "./proactive-alert";
|
||||
|
||||
// Trending Tokens - displays hot/trending tokens
|
||||
export {
|
||||
TrendingTokensToolUI,
|
||||
TrendingTokensArgsSchema,
|
||||
TrendingTokensResultSchema,
|
||||
type TrendingTokensArgs,
|
||||
type TrendingTokensResult,
|
||||
} from "./trending-tokens";
|
||||
|
||||
// Whale Activity - displays whale transactions
|
||||
export {
|
||||
WhaleActivityToolUI,
|
||||
WhaleActivityArgsSchema,
|
||||
WhaleActivityResultSchema,
|
||||
type WhaleActivityArgs,
|
||||
type WhaleActivityResult,
|
||||
} from "./whale-activity";
|
||||
|
||||
// Market Overview - displays market summary
|
||||
export {
|
||||
MarketOverviewToolUI,
|
||||
MarketOverviewArgsSchema,
|
||||
MarketOverviewResultSchema,
|
||||
type MarketOverviewArgs,
|
||||
type MarketOverviewResult,
|
||||
} from "./market-overview-tool";
|
||||
|
||||
// Holder Analysis - displays holder distribution
|
||||
export {
|
||||
HolderAnalysisToolUI,
|
||||
HolderAnalysisArgsSchema,
|
||||
HolderAnalysisResultSchema,
|
||||
type HolderAnalysisArgs,
|
||||
type HolderAnalysisResult,
|
||||
} from "./holder-analysis";
|
||||
|
||||
// Portfolio Display - displays user's portfolio
|
||||
export {
|
||||
PortfolioDisplayToolUI,
|
||||
PortfolioDisplayArgsSchema,
|
||||
PortfolioDisplayResultSchema,
|
||||
type PortfolioDisplayArgs,
|
||||
type PortfolioDisplayResult,
|
||||
} from "./portfolio-display";
|
||||
|
||||
// User Profile - displays user's investment profile
|
||||
export {
|
||||
UserProfileToolUI,
|
||||
UserProfileArgsSchema,
|
||||
UserProfileResultSchema,
|
||||
type UserProfileArgs,
|
||||
type UserProfileResult,
|
||||
} from "./user-profile";
|
||||
|
||||
// =========================================================================
|
||||
// REAL-TIME CRYPTO TOOLS - Hybrid approach (RAG + Real-time)
|
||||
// =========================================================================
|
||||
// These components render results from real-time DexScreener API calls.
|
||||
// Used alongside RAG-based tools for comprehensive crypto analysis.
|
||||
|
||||
// Live Token Price - displays real-time price from DexScreener
|
||||
export {
|
||||
LiveTokenPriceToolUI,
|
||||
LiveTokenPriceArgsSchema,
|
||||
LiveTokenPriceResultSchema,
|
||||
type LiveTokenPriceArgs,
|
||||
type LiveTokenPriceResult,
|
||||
} from "./live-token-price";
|
||||
|
||||
// Live Token Data - displays comprehensive real-time market data
|
||||
export {
|
||||
LiveTokenDataToolUI,
|
||||
LiveTokenDataArgsSchema,
|
||||
LiveTokenDataResultSchema,
|
||||
type LiveTokenDataArgs,
|
||||
type LiveTokenDataResult,
|
||||
} from "./live-token-data";
|
||||
246
surfsense_web/components/tool-ui/crypto/live-token-data.tsx
Normal file
246
surfsense_web/components/tool-ui/crypto/live-token-data.tsx
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
"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>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
161
surfsense_web/components/tool-ui/crypto/live-token-price.tsx
Normal file
161
surfsense_web/components/tool-ui/crypto/live-token-price.tsx
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
"use client";
|
||||
|
||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import { z } from "zod";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TrendingUp, TrendingDown, ExternalLink, Zap, RefreshCw } 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 price tool arguments
|
||||
export const LiveTokenPriceArgsSchema = z.object({
|
||||
chain: z.string(),
|
||||
token_address: z.string(),
|
||||
token_symbol: z.string().optional(),
|
||||
});
|
||||
|
||||
export type LiveTokenPriceArgs = z.infer<typeof LiveTokenPriceArgsSchema>;
|
||||
|
||||
// Schema for live token price result (matches backend response)
|
||||
export const LiveTokenPriceResultSchema = z.object({
|
||||
id: z.string(),
|
||||
kind: z.literal("live_token_price"),
|
||||
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(),
|
||||
liquidity_usd: z.number().optional(),
|
||||
market_cap: z.number().optional(),
|
||||
fdv: 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 LiveTokenPriceResult = z.infer<typeof LiveTokenPriceResultSchema>;
|
||||
|
||||
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) 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 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>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* LiveTokenPriceToolUI - Displays real-time token price from DexScreener
|
||||
* Used when AI fetches current/live price data
|
||||
*/
|
||||
export const LiveTokenPriceToolUI = makeAssistantToolUI<LiveTokenPriceArgs, LiveTokenPriceResult>({
|
||||
toolName: "get_live_token_price",
|
||||
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");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="my-3 overflow-hidden border-blue-500/20">
|
||||
<CardHeader className="pb-3 bg-gradient-to-r from-blue-500/5 to-transparent">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Zap className="h-5 w-5 text-blue-500" />
|
||||
Live Price
|
||||
{isLoading && <Badge variant="secondary" className="animate-pulse">Fetching...</Badge>}
|
||||
{!isLoading && !hasError && (
|
||||
<Badge variant="outline" className="text-xs text-blue-500 border-blue-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>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
"private": true,
|
||||
"description": "SurfSense Frontend",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev": "next dev --turbopack -p 3999",
|
||||
"dev:turbo": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue