diff --git a/_bmad-output/planning-artifacts/ux-design-specification.md b/_bmad-output/planning-artifacts/ux-design-specification.md index fa62c51f8..5f6dde5d3 100644 --- a/_bmad-output/planning-artifacts/ux-design-specification.md +++ b/_bmad-output/planning-artifacts/ux-design-specification.md @@ -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. --- - - - -## 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. diff --git a/surfsense_backend/app/agents/new_chat/tools/__init__.py b/surfsense_backend/app/agents/new_chat/tools/__init__.py index 9e1a4f19c..3b29dd180 100644 --- a/surfsense_backend/app/agents/new_chat/tools/__init__.py +++ b/surfsense_backend/app/agents/new_chat/tools/__init__.py @@ -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", diff --git a/surfsense_backend/app/agents/new_chat/tools/crypto_realtime.py b/surfsense_backend/app/agents/new_chat/tools/crypto_realtime.py new file mode 100644 index 000000000..52006f03e --- /dev/null +++ b/surfsense_backend/app/agents/new_chat/tools/crypto_realtime.py @@ -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 + diff --git a/surfsense_backend/app/agents/new_chat/tools/registry.py b/surfsense_backend/app/agents/new_chat/tools/registry.py index c65445419..f4d0b008b 100644 --- a/surfsense_backend/app/agents/new_chat/tools/registry.py +++ b/surfsense_backend/app/agents/new_chat/tools/registry.py @@ -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=[], + ), ] diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index 803bd6661..c3831851b 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -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() { + {/* Crypto Tool UI Components - Conversational Crypto Advisor */} + + + + + + + + + + + + {/* Real-time Crypto Tools - Hybrid approach (RAG + Real-time) */} + + {/* Disabled for now */}
; + +// 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; + +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 ( +
+

{label}

+

+ {isPositive ? "+" : ""}{value.toFixed(2)}% +

+
+ ); +}; + +/** + * LiveTokenDataToolUI - Displays comprehensive real-time market data + * Used when AI fetches detailed live market information + */ +export const LiveTokenDataToolUI = makeAssistantToolUI({ + 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 ( + + + + + Live Market Data + {isLoading && Fetching...} + {!isLoading && !hasError && ( + + + Real-time + + )} + + + + {hasError ? ( +
+ ⚠️ {result.error} +
+ ) : ( + <> + {/* Token Header */} +
+
+ +
+
+ + {result?.token_symbol || args.token_symbol || "Token"} + + {result?.token_name && ( + {result.token_name} + )} +
+
+ {formatPrice(result?.price_usd)} + {result?.price_change_24h !== undefined && ( + = 0 ? "text-green-500" : "text-red-500" + )}> + {result.price_change_24h >= 0 ? : } + {result.price_change_24h >= 0 ? "+" : ""}{result.price_change_24h.toFixed(2)}% + + )} +
+
+
+
+ + {/* Price Changes */} +
+ + + + +
+ + {/* Metrics Grid */} +
+
+

+ 24h Volume +

+

{formatLargeNumber(result?.volume_24h)}

+
+
+

+ Liquidity +

+

{formatLargeNumber(result?.liquidity_usd)}

+
+
+

Market Cap

+

{formatLargeNumber(result?.market_cap)}

+
+
+

FDV

+

{formatLargeNumber(result?.fdv)}

+
+
+ + {/* Transaction Activity */} +
+

+ 24h Transactions +

+
+
+
+
+
+
+ + {formatNumber(result?.txns_24h_buys)} buys + + + {formatNumber(totalTxns24h)} total + + + {formatNumber(result?.txns_24h_sells)} sells + +
+
+ + {/* DEX Info & Actions */} +
+
+ DEX: {result?.dex || "Unknown"} + {result?.total_pairs && result.total_pairs > 1 && ( + • {result.total_pairs} pairs + )} +
+ +
+ + )} + + + ); + }, +}); + diff --git a/surfsense_web/components/tool-ui/crypto/live-token-price.tsx b/surfsense_web/components/tool-ui/crypto/live-token-price.tsx new file mode 100644 index 000000000..3c8a586b8 --- /dev/null +++ b/surfsense_web/components/tool-ui/crypto/live-token-price.tsx @@ -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; + +// 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; + +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 ( +
+

{label}

+

+ {isPositive ? "+" : ""}{value.toFixed(2)}% +

+
+ ); +}; + +/** + * LiveTokenPriceToolUI - Displays real-time token price from DexScreener + * Used when AI fetches current/live price data + */ +export const LiveTokenPriceToolUI = makeAssistantToolUI({ + 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 ( + + + + + Live Price + {isLoading && Fetching...} + {!isLoading && !hasError && ( + + + Real-time + + )} + + + + {hasError ? ( +
+ ⚠️ {result.error} +
+ ) : ( + <> + {/* Token Header */} +
+
+ +
+
+ + {result?.token_symbol || args.token_symbol || "Token"} + + {result?.token_name && ( + {result.token_name} + )} +
+
+ {formatPrice(result?.price_usd)} + {result?.price_change_24h !== undefined && ( + = 0 ? "text-green-500" : "text-red-500" + )}> + {result.price_change_24h >= 0 ? : } + {result.price_change_24h >= 0 ? "+" : ""}{result.price_change_24h.toFixed(2)}% + + )} +
+
+
+
+ + {/* Price Changes */} +
+ + + + +
+ + )} +
+
+ ); + }, +}); + diff --git a/surfsense_web/package.json b/surfsense_web/package.json index aff35f0af..77ea0b5b2 100644 --- a/surfsense_web/package.json +++ b/surfsense_web/package.json @@ -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",