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:
API Test Bot 2026-02-04 00:12:32 +07:00
parent f2e38c52a1
commit d20cb8a538
9 changed files with 979 additions and 125 deletions

View file

@ -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.

View file

@ -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",

View 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

View file

@ -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=[],
),
]

View file

@ -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

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

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

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

View file

@ -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",