-
-
Price
-
{tokenData.price || "β"}
-
+ {/* Price with change indicator */}
+
+ {tokenData.price || "β"}
+ {priceChange !== undefined && (
+
+ {isPositive && }
+ {isNegative && }
+ {isPositive ? "+" : ""}{priceChange.toFixed(2)}%
+
+ )}
+
+
+ {/* Token stats grid - now with 4 columns including market cap */}
+
24h Vol
-
{tokenData.volume24h || "β"}
+
{tokenData.volume24h || "β"}
Liquidity
-
{tokenData.liquidity || "β"}
+
{tokenData.liquidity || "β"}
+
+
+
MCap
+
{tokenData.marketCap || "β"}
+
+
+
Chain
+
{tokenData.chain}
- {/* Quick actions */}
-
+ {/* Quick actions - now with 4 buttons */}
+
handleQuickAction("safety")}
>
@@ -62,7 +193,7 @@ export function TokenInfoCard({ tokenData }: TokenInfoCardProps) {
handleQuickAction("holders")}
>
@@ -71,12 +202,21 @@ export function TokenInfoCard({ tokenData }: TokenInfoCardProps) {
handleQuickAction("prediction")}
+ className="h-8 px-2 text-xs"
+ onClick={() => handleQuickAction("predict")}
>
Predict
+ handleQuickAction("rug")}
+ >
+
+ Rug
+
);
diff --git a/surfsense_browser_extension/sidepanel/mock/mockData.ts b/surfsense_browser_extension/sidepanel/mock/mockData.ts
new file mode 100644
index 000000000..6cf4735d2
--- /dev/null
+++ b/surfsense_browser_extension/sidepanel/mock/mockData.ts
@@ -0,0 +1,280 @@
+/**
+ * Mock data for testing SurfSense Extension UI
+ * Remove or disable in production
+ */
+
+import type { TokenData } from "../context/PageContextProvider";
+import type { WatchlistToken, WatchlistAlert } from "../crypto/WatchlistPanel";
+import type { SafetyFactor } from "../crypto/SafetyScoreDisplay";
+import type { AlertConfig } from "../crypto/AlertConfigModal";
+
+// ============================================
+// MOCK TOKEN DATA (DexScreener)
+// ============================================
+
+export const MOCK_TOKEN_DATA: TokenData & {
+ priceChange24h: number;
+ marketCap: string;
+ tokenName: string;
+} = {
+ chain: "solana",
+ pairAddress: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
+ tokenSymbol: "BULLA",
+ tokenName: "Bulla Token",
+ price: "$0.00001234",
+ priceChange24h: 156.7,
+ volume24h: "$1.2M",
+ liquidity: "$450K",
+ marketCap: "$2.1M",
+};
+
+export const MOCK_TOKEN_DATA_BEARISH: TokenData & {
+ priceChange24h: number;
+ marketCap: string;
+ tokenName: string;
+} = {
+ chain: "ethereum",
+ pairAddress: "0x1234567890abcdef1234567890abcdef12345678",
+ tokenSymbol: "REKT",
+ tokenName: "Rekt Token",
+ price: "$0.00000042",
+ priceChange24h: -78.5,
+ volume24h: "$89K",
+ liquidity: "$12K",
+ marketCap: "$156K",
+};
+
+// ============================================
+// MOCK SAFETY ANALYSIS
+// ============================================
+
+export const MOCK_SAFETY_SCORE = 72;
+
+export const MOCK_SAFETY_FACTORS: SafetyFactor[] = [
+ // Positive factors
+ {
+ category: "Liquidity",
+ status: "positive",
+ description: "Liquidity pool is locked for 12 months",
+ },
+ {
+ category: "Contract",
+ status: "positive",
+ description: "Contract is verified on Solscan",
+ },
+ {
+ category: "Age",
+ status: "positive",
+ description: "Token has been active for 45 days",
+ },
+ // Warning factors
+ {
+ category: "Holders",
+ status: "warning",
+ description: "Top 10 holders own 35% of supply",
+ },
+ {
+ category: "Volume",
+ status: "warning",
+ description: "Trading volume decreased 40% in last 24h",
+ },
+ // Danger factors
+ {
+ category: "Mint Authority",
+ status: "danger",
+ description: "Mint authority is NOT revoked - tokens can be minted",
+ },
+];
+
+export const MOCK_SAFETY_SOURCES = [
+ "RugCheck.xyz",
+ "GoPlus Security",
+ "Solscan",
+ "DexScreener",
+];
+
+// ============================================
+// MOCK WATCHLIST
+// ============================================
+
+export const MOCK_WATCHLIST_TOKENS: WatchlistToken[] = [
+ {
+ id: "1",
+ symbol: "BULLA",
+ name: "Bulla Token",
+ chain: "solana",
+ contractAddress: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
+ price: "$0.00001234",
+ priceChange24h: 156.7,
+ hasAlerts: true,
+ alertCount: 2,
+ },
+ {
+ id: "2",
+ symbol: "BONK",
+ name: "Bonk",
+ chain: "solana",
+ contractAddress: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
+ price: "$0.00002156",
+ priceChange24h: 12.3,
+ hasAlerts: true,
+ alertCount: 1,
+ },
+ {
+ id: "3",
+ symbol: "WIF",
+ name: "dogwifhat",
+ chain: "solana",
+ contractAddress: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm",
+ price: "$2.45",
+ priceChange24h: -5.2,
+ hasAlerts: false,
+ },
+ {
+ id: "4",
+ symbol: "PEPE",
+ name: "Pepe",
+ chain: "ethereum",
+ contractAddress: "0x6982508145454Ce325dDbE47a25d4ec3d2311933",
+ price: "$0.00001089",
+ priceChange24h: 8.7,
+ hasAlerts: false,
+ },
+ {
+ id: "5",
+ symbol: "DEGEN",
+ name: "Degen",
+ chain: "base",
+ contractAddress: "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed",
+ price: "$0.0156",
+ priceChange24h: -15.3,
+ hasAlerts: true,
+ alertCount: 3,
+ },
+];
+
+export const MOCK_WATCHLIST_ALERTS: WatchlistAlert[] = [
+ {
+ id: "alert-1",
+ tokenSymbol: "BULLA",
+ type: "price",
+ message: "BULLA price increased above $0.00001 (+156%)",
+ timestamp: new Date(Date.now() - 1000 * 60 * 5), // 5 mins ago
+ isRead: false,
+ },
+ {
+ id: "alert-2",
+ tokenSymbol: "BULLA",
+ type: "whale",
+ message: "Large transaction detected: 500M BULLA ($6,170) transferred",
+ timestamp: new Date(Date.now() - 1000 * 60 * 15), // 15 mins ago
+ isRead: false,
+ },
+ {
+ id: "alert-3",
+ tokenSymbol: "DEGEN",
+ type: "volume",
+ message: "DEGEN volume spike: 5x average in last hour",
+ timestamp: new Date(Date.now() - 1000 * 60 * 30), // 30 mins ago
+ isRead: true,
+ },
+ {
+ id: "alert-4",
+ tokenSymbol: "BONK",
+ type: "liquidity",
+ message: "BONK liquidity increased by 25% ($1.2M added)",
+ timestamp: new Date(Date.now() - 1000 * 60 * 60), // 1 hour ago
+ isRead: true,
+ },
+ {
+ id: "alert-5",
+ tokenSymbol: "DEGEN",
+ type: "price",
+ message: "DEGEN dropped below $0.02 (-15%)",
+ timestamp: new Date(Date.now() - 1000 * 60 * 120), // 2 hours ago
+ isRead: true,
+ },
+];
+
+// ============================================
+// MOCK ALERT CONFIGS
+// ============================================
+
+export const MOCK_ALERT_CONFIGS: AlertConfig[] = [
+ { type: "price_above", threshold: 0.00002, enabled: true },
+ { type: "price_below", threshold: 0.000005, enabled: true },
+ { type: "price_change", threshold: 20, enabled: false },
+ { type: "volume", threshold: 3, enabled: true },
+ { type: "whale", threshold: 10000, enabled: false },
+ { type: "liquidity", threshold: 30, enabled: false },
+ { type: "holder_concentration", threshold: 50, enabled: false },
+];
+
+// ============================================
+// MOCK SEARCH SPACES
+// ============================================
+
+export const MOCK_SEARCH_SPACES = [
+ { id: "crypto", name: "Crypto", icon: "πͺ" },
+ { id: "general", name: "General", icon: "π" },
+ { id: "research", name: "Research", icon: "π¬" },
+ { id: "defi", name: "DeFi", icon: "π°" },
+ { id: "nft", name: "NFT", icon: "πΌοΈ" },
+];
+
+// ============================================
+// MOCK CHAT MESSAGES
+// ============================================
+
+export const MOCK_CHAT_MESSAGES = [
+ {
+ id: "msg-1",
+ role: "user" as const,
+ content: "Is BULLA token safe to invest?",
+ timestamp: new Date(Date.now() - 1000 * 60 * 5),
+ },
+ {
+ id: "msg-2",
+ role: "assistant" as const,
+ content: `Based on my analysis of BULLA token, here's what I found:
+
+**Safety Score: 72/100** β οΈ Medium Risk
+
+**β
Positive Signals:**
+- Liquidity is locked for 12 months
+- Contract is verified
+- Active for 45 days
+
+**β οΈ Warnings:**
+- Top 10 holders own 35% of supply
+- Volume decreased 40% recently
+
+**π¨ Red Flags:**
+- Mint authority is NOT revoked
+
+**Recommendation:** Proceed with caution. The unlocked mint authority is a significant risk factor.`,
+ timestamp: new Date(Date.now() - 1000 * 60 * 4),
+ thinkingSteps: [
+ { id: "1", type: "thinking" as const, title: "Understanding your question...", isComplete: true },
+ { id: "2", type: "searching" as const, title: "Fetching token data from DexScreener...", isComplete: true },
+ { id: "3", type: "analyzing" as const, title: "Running safety analysis...", isComplete: true },
+ { id: "4", type: "complete" as const, title: "Analysis complete", isComplete: true },
+ ],
+ },
+];
+
+// ============================================
+// FEATURE FLAGS
+// ============================================
+
+export const MOCK_MODE = {
+ /** Enable mock data for development/testing */
+ enabled: true,
+ /** Simulate DexScreener page context */
+ simulateDexScreener: true,
+ /** Show mock watchlist data */
+ showWatchlist: true,
+ /** Show mock alerts */
+ showAlerts: true,
+};
+
diff --git a/surfsense_browser_extension/sidepanel/widgets/ActionConfirmationWidget.tsx b/surfsense_browser_extension/sidepanel/widgets/ActionConfirmationWidget.tsx
new file mode 100644
index 000000000..933ddce22
--- /dev/null
+++ b/surfsense_browser_extension/sidepanel/widgets/ActionConfirmationWidget.tsx
@@ -0,0 +1,125 @@
+import { cn } from "~/lib/utils";
+import { CheckCircle, Bell, Eye, Settings } from "lucide-react";
+import { Button } from "@/routes/ui/button";
+
+export interface ActionConfirmationProps {
+ /** Type of action that was confirmed */
+ actionType: "watchlist_add" | "watchlist_remove" | "alert_set" | "alert_delete";
+ /** Token symbol */
+ tokenSymbol: string;
+ /** Additional details about the action */
+ details?: string[];
+ /** Callback when view watchlist is clicked */
+ onViewWatchlist?: () => void;
+ /** Callback when edit alerts is clicked */
+ onEditAlerts?: () => void;
+ /** Additional class names */
+ className?: string;
+}
+
+/**
+ * ActionConfirmationWidget - Embedded widget showing action confirmation in chat
+ *
+ * Used when AI executes an action like adding to watchlist or setting alerts.
+ * Displays confirmation with relevant follow-up actions.
+ */
+export function ActionConfirmationWidget({
+ actionType,
+ tokenSymbol,
+ details = [],
+ onViewWatchlist,
+ onEditAlerts,
+ className,
+}: ActionConfirmationProps) {
+ const getActionTitle = () => {
+ switch (actionType) {
+ case "watchlist_add":
+ return `${tokenSymbol} added to your watchlist`;
+ case "watchlist_remove":
+ return `${tokenSymbol} removed from watchlist`;
+ case "alert_set":
+ return `Alert configured for ${tokenSymbol}`;
+ case "alert_delete":
+ return `Alert removed for ${tokenSymbol}`;
+ default:
+ return "Action completed";
+ }
+ };
+
+ const getIcon = () => {
+ switch (actionType) {
+ case "watchlist_add":
+ case "watchlist_remove":
+ return Eye;
+ case "alert_set":
+ case "alert_delete":
+ return Bell;
+ default:
+ return CheckCircle;
+ }
+ };
+
+ const Icon = getIcon();
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
Action Confirmed
+
+
+ {/* Action details */}
+
+
+
+ {getActionTitle()}
+
+
+ {details.length > 0 && (
+
+
Also set up:
+ {details.map((detail, index) => (
+
+
+ {detail}
+
+ ))}
+
+ )}
+
+
+ {/* Action buttons */}
+
+ {(actionType === "watchlist_add" || actionType === "watchlist_remove") && (
+
+
+ View Watchlist
+
+ )}
+ {(actionType === "watchlist_add" || actionType === "alert_set") && (
+
+
+ Edit Alerts
+
+ )}
+
+
+ );
+}
+
diff --git a/surfsense_browser_extension/sidepanel/widgets/AlertWidget.tsx b/surfsense_browser_extension/sidepanel/widgets/AlertWidget.tsx
new file mode 100644
index 000000000..99a9eee04
--- /dev/null
+++ b/surfsense_browser_extension/sidepanel/widgets/AlertWidget.tsx
@@ -0,0 +1,148 @@
+import { cn } from "~/lib/utils";
+import { Bell, CheckCircle, Edit, Trash2, Plus } from "lucide-react";
+import { Button } from "@/routes/ui/button";
+
+export interface AlertConfigData {
+ /** Token symbol */
+ tokenSymbol: string;
+ /** Alert condition description */
+ condition: string;
+ /** Current price */
+ currentPrice?: string;
+ /** Trigger price */
+ triggerPrice?: string;
+ /** Notification channels */
+ channels: {
+ browser: boolean;
+ inApp: boolean;
+ email: boolean;
+ };
+}
+
+export interface AlertWidgetProps {
+ /** Alert configuration data */
+ config: AlertConfigData;
+ /** Whether this is a new alert or existing */
+ isNew?: boolean;
+ /** Callback when edit is clicked */
+ onEdit?: () => void;
+ /** Callback when delete is clicked */
+ onDelete?: () => void;
+ /** Callback when add another is clicked */
+ onAddAnother?: () => void;
+ /** Callback when view all alerts is clicked */
+ onViewAll?: () => void;
+ /** Additional class names */
+ className?: string;
+}
+
+/**
+ * AlertWidget - Embedded alert configuration widget for chat
+ *
+ * Shows alert configuration inline in chat after user sets an alert
+ * via natural language command.
+ */
+export function AlertWidget({
+ config,
+ isNew = true,
+ onEdit,
+ onDelete,
+ onAddAnother,
+ onViewAll,
+ className,
+}: AlertWidgetProps) {
+ return (
+
+ {/* Header */}
+
+
+ {isNew ? (
+
+ ) : (
+
+ )}
+
+
+ {isNew ? "Alert Created" : "AlertWidget"}
+
+
+
+ {/* Alert details */}
+
+
+ Token:
+ {config.tokenSymbol}
+
+
+ Condition:
+ {config.condition}
+
+ {config.currentPrice && (
+
+ Current:
+ {config.currentPrice}
+
+ )}
+ {config.triggerPrice && (
+
+ Trigger at:
+ {config.triggerPrice}
+
+ )}
+
+ {/* Notification channels */}
+
+
Notify via:
+
+
+ {config.channels.browser ? "β" : "β"} Browser
+
+
+ {config.channels.inApp ? "β" : "β"} In-app
+
+
+ {config.channels.email ? "β" : "β"} Email
+
+
+
+
+
+ {/* Action buttons */}
+
+
+
+ Edit
+
+
+
+
+
+
+
+
+
+ {/* View all link */}
+ {onViewAll && (
+
+ View all alerts β
+
+ )}
+
+ );
+}
+
diff --git a/surfsense_browser_extension/sidepanel/widgets/ProactiveAlertCard.tsx b/surfsense_browser_extension/sidepanel/widgets/ProactiveAlertCard.tsx
new file mode 100644
index 000000000..60341afa4
--- /dev/null
+++ b/surfsense_browser_extension/sidepanel/widgets/ProactiveAlertCard.tsx
@@ -0,0 +1,184 @@
+import { cn } from "~/lib/utils";
+import { AlertTriangle, TrendingUp, Info, X, Bell, ChevronRight } from "lucide-react";
+import { Button } from "@/routes/ui/button";
+
+export interface ProactiveAlertData {
+ /** Alert ID */
+ id: string;
+ /** Alert type */
+ type: "price_pump" | "price_dump" | "whale_activity" | "volume_spike" | "safety_warning";
+ /** Token symbol */
+ tokenSymbol: string;
+ /** Alert title */
+ title: string;
+ /** Current price */
+ currentPrice?: string;
+ /** User's entry price (if applicable) */
+ entryPrice?: string;
+ /** User's P&L (if applicable) */
+ pnl?: string;
+ /** Warning messages */
+ warnings?: string[];
+ /** When the alert was triggered */
+ timestamp: Date;
+}
+
+export interface ProactiveAlertCardProps {
+ /** Alert data */
+ alert: ProactiveAlertData;
+ /** AI's recommendation text */
+ recommendation?: string;
+ /** Callback when view details is clicked */
+ onViewDetails?: () => void;
+ /** Callback when dismiss is clicked */
+ onDismiss?: () => void;
+ /** Callback when set alert is clicked */
+ onSetAlert?: () => void;
+ /** Callback when tell me more is clicked */
+ onTellMore?: () => void;
+ /** Additional class names */
+ className?: string;
+}
+
+/**
+ * ProactiveAlertCard - AI-initiated alert card embedded in chat
+ *
+ * Displays proactive alerts from the AI about price movements,
+ * whale activity, or safety concerns. Shows user's position if applicable.
+ */
+export function ProactiveAlertCard({
+ alert,
+ recommendation,
+ onViewDetails,
+ onDismiss,
+ onSetAlert,
+ onTellMore,
+ className,
+}: ProactiveAlertCardProps) {
+ const getAlertIcon = () => {
+ switch (alert.type) {
+ case "price_pump":
+ case "price_dump":
+ return TrendingUp;
+ case "whale_activity":
+ case "volume_spike":
+ return AlertTriangle;
+ case "safety_warning":
+ return AlertTriangle;
+ default:
+ return Info;
+ }
+ };
+
+ const getAlertColor = () => {
+ switch (alert.type) {
+ case "price_pump":
+ return "text-green-500 bg-green-500/10";
+ case "price_dump":
+ case "safety_warning":
+ return "text-red-500 bg-red-500/10";
+ case "whale_activity":
+ case "volume_spike":
+ return "text-yellow-500 bg-yellow-500/10";
+ default:
+ return "text-primary bg-primary/10";
+ }
+ };
+
+ const Icon = getAlertIcon();
+ const colorClass = getAlertColor();
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
π¨ ProactiveAlertCard
+
+ {alert.timestamp.toLocaleTimeString()}
+
+
+
+
+
+
+
+
+ {/* Alert content */}
+
+
{alert.title}
+
+ {/* Price info */}
+ {(alert.currentPrice || alert.entryPrice || alert.pnl) && (
+
+ {alert.currentPrice && (
+
+ π Current:
+ {alert.currentPrice}
+
+ )}
+ {alert.entryPrice && (
+
+ π Your entry:
+ {alert.entryPrice}
+
+ )}
+ {alert.pnl && (
+
+ π° Your P&L:
+ {alert.pnl}
+
+ )}
+
+ )}
+
+ {/* Warnings */}
+ {alert.warnings && alert.warnings.length > 0 && (
+
+ {alert.warnings.map((warning, index) => (
+
+ ))}
+
+ )}
+
+
+ {/* AI Recommendation */}
+ {recommendation && (
+
+ {recommendation}
+
+ )}
+
+ {/* Action buttons */}
+
+
+
+ Tell me more
+
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/surfsense_browser_extension/sidepanel/widgets/TokenAnalysisWidget.tsx b/surfsense_browser_extension/sidepanel/widgets/TokenAnalysisWidget.tsx
new file mode 100644
index 000000000..6e06a0311
--- /dev/null
+++ b/surfsense_browser_extension/sidepanel/widgets/TokenAnalysisWidget.tsx
@@ -0,0 +1,196 @@
+import { cn } from "~/lib/utils";
+import { Shield, TrendingUp, TrendingDown, Users, AlertTriangle, CheckCircle, Star, Bell } from "lucide-react";
+import { Button } from "@/routes/ui/button";
+import { ChainIcon } from "../components/shared/ChainIcon";
+
+export interface TokenAnalysisData {
+ /** Token symbol */
+ symbol: string;
+ /** Token name */
+ name?: string;
+ /** Blockchain */
+ chain: string;
+ /** Current price */
+ price: string;
+ /** 24h price change */
+ priceChange24h: number;
+ /** Market cap */
+ marketCap?: string;
+ /** 24h volume */
+ volume24h?: string;
+ /** Liquidity */
+ liquidity?: string;
+ /** Safety score (0-100) */
+ safetyScore?: number;
+ /** Holder count */
+ holderCount?: number;
+ /** Top 10 holder percentage */
+ top10HolderPercent?: number;
+}
+
+export interface TokenAnalysisWidgetProps {
+ /** Token analysis data */
+ data: TokenAnalysisData;
+ /** Whether token is in watchlist */
+ isInWatchlist?: boolean;
+ /** Callback when add to watchlist is clicked */
+ onAddToWatchlist?: () => void;
+ /** Callback when set alert is clicked */
+ onSetAlert?: () => void;
+ /** Callback when analyze further is clicked */
+ onAnalyzeFurther?: () => void;
+ /** Additional class names */
+ className?: string;
+}
+
+/**
+ * TokenAnalysisWidget - Full token analysis card embedded in chat
+ *
+ * Displays comprehensive token analysis including price, safety score,
+ * and key metrics. Used when AI responds to token research queries.
+ */
+export function TokenAnalysisWidget({
+ data,
+ isInWatchlist = false,
+ onAddToWatchlist,
+ onSetAlert,
+ onAnalyzeFurther,
+ className,
+}: TokenAnalysisWidgetProps) {
+ const getSafetyColor = (score?: number) => {
+ if (!score) return "text-muted-foreground";
+ if (score >= 80) return "text-green-500";
+ if (score >= 60) return "text-yellow-500";
+ return "text-red-500";
+ };
+
+ const getSafetyLabel = (score?: number) => {
+ if (!score) return "Unknown";
+ if (score >= 80) return "Low Risk";
+ if (score >= 60) return "Medium Risk";
+ return "High Risk";
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ π
+ TokenAnalysisCard
+
+
+
+
+ {/* Token info */}
+
+
+ πͺ
+
+
+
+ {data.symbol}
+ {data.name && (
+ {data.name}
+ )}
+
+
+ {data.price}
+ = 0 ? "text-green-500" : "text-red-500"
+ )}>
+ {data.priceChange24h >= 0 ? (
+
+ ) : (
+
+ )}
+ {data.priceChange24h >= 0 ? "+" : ""}{data.priceChange24h.toFixed(1)}%
+
+
+
+
+
+
+
+
+ {/* Metrics grid */}
+
+ {data.marketCap && (
+
+
Market Cap
+
{data.marketCap}
+
+ )}
+ {data.volume24h && (
+
+
24h Volume
+
{data.volume24h}
+
+ )}
+ {data.liquidity && (
+
+
Liquidity
+
{data.liquidity}
+
+ )}
+ {data.safetyScore !== undefined && (
+
+
Safety Score
+
+
+ {data.safetyScore}/100 ({getSafetyLabel(data.safetyScore)})
+
+
+ )}
+
+
+ {/* Holder info */}
+ {(data.holderCount || data.top10HolderPercent) && (
+
+ {data.holderCount && (
+
+
+ {data.holderCount.toLocaleString()} holders
+
+ )}
+ {data.top10HolderPercent && (
+
50 ? "text-yellow-500" : ""
+ )}>
+ {data.top10HolderPercent > 50 && }
+ Top 10: {data.top10HolderPercent}%
+
+ )}
+
+ )}
+
+ {/* Action buttons */}
+
+
+
+ {isInWatchlist ? "In Watchlist" : "Add to Watchlist"}
+
+
+
+
+
+ Analyze More
+
+
+
+ );
+}
+
diff --git a/surfsense_browser_extension/sidepanel/widgets/WatchlistWidget.tsx b/surfsense_browser_extension/sidepanel/widgets/WatchlistWidget.tsx
new file mode 100644
index 000000000..d1ddc4957
--- /dev/null
+++ b/surfsense_browser_extension/sidepanel/widgets/WatchlistWidget.tsx
@@ -0,0 +1,151 @@
+import { cn } from "~/lib/utils";
+import { TrendingUp, TrendingDown, Bell, Trash2, Search, Plus } from "lucide-react";
+import { Button } from "@/routes/ui/button";
+import { ChainIcon } from "../components/shared/ChainIcon";
+
+export interface WatchlistItem {
+ id: string;
+ symbol: string;
+ name?: string;
+ chain: string;
+ price: string;
+ priceChange24h: number;
+ alertCount?: number;
+}
+
+export interface WatchlistWidgetProps {
+ /** List of tokens in watchlist */
+ tokens: WatchlistItem[];
+ /** Callback when analyze token is clicked */
+ onAnalyze?: (token: WatchlistItem) => void;
+ /** Callback when remove token is clicked */
+ onRemove?: (tokenId: string) => void;
+ /** Callback when add token is clicked */
+ onAddToken?: () => void;
+ /** Callback when clear all is clicked */
+ onClearAll?: () => void;
+ /** Additional class names */
+ className?: string;
+}
+
+/**
+ * WatchlistWidget - Embedded watchlist widget for chat interface
+ *
+ * Displays user's watchlist inline in the chat conversation.
+ * Supports quick actions like analyze, remove, and add tokens.
+ */
+export function WatchlistWidget({
+ tokens,
+ onAnalyze,
+ onRemove,
+ onAddToken,
+ onClearAll,
+ className,
+}: WatchlistWidgetProps) {
+ if (tokens.length === 0) {
+ return (
+
+
+ π
+ Your Watchlist
+
+
+ Your watchlist is empty. Add tokens to track them!
+
+
+
+ Add Token
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+ π
+ WatchlistWidget
+
+
{tokens.length} tokens
+
+
+ {/* Token list */}
+
+ {tokens.map((token) => (
+
+ {/* Token info */}
+
+
+ {token.symbol}
+
+ {token.alertCount && token.alertCount > 0 && (
+
+
+ {token.alertCount}
+
+ )}
+
+
{token.price}
+
+
+ {/* Price change */}
+
= 0 ? "text-green-500" : "text-red-500"
+ )}>
+ {token.priceChange24h >= 0 ? (
+
+ ) : (
+
+ )}
+ {token.priceChange24h >= 0 ? "+" : ""}{token.priceChange24h.toFixed(1)}%
+
+
+ {/* Actions */}
+
+ onAnalyze?.(token)}
+ title="Analyze"
+ >
+
+
+ onRemove?.(token.id)}
+ title="Remove"
+ >
+
+
+
+
+ ))}
+
+
+ {/* Footer actions */}
+
+
+
+ Add Token
+
+ {tokens.length > 0 && (
+
+ Clear All
+
+ )}
+
+
+ );
+}
+
diff --git a/surfsense_browser_extension/sidepanel/widgets/index.ts b/surfsense_browser_extension/sidepanel/widgets/index.ts
new file mode 100644
index 000000000..2c92ccb39
--- /dev/null
+++ b/surfsense_browser_extension/sidepanel/widgets/index.ts
@@ -0,0 +1,9 @@
+// Conversational UX Widgets for SurfSense Browser Extension
+// These widgets are embedded inline in chat messages for a conversation-first experience
+
+export { ActionConfirmationWidget, type ActionConfirmationProps } from "./ActionConfirmationWidget";
+export { ProactiveAlertCard, type ProactiveAlertCardProps, type ProactiveAlertData } from "./ProactiveAlertCard";
+export { WatchlistWidget, type WatchlistWidgetProps, type WatchlistItem } from "./WatchlistWidget";
+export { AlertWidget, type AlertWidgetProps, type AlertConfigData } from "./AlertWidget";
+export { TokenAnalysisWidget, type TokenAnalysisWidgetProps, type TokenAnalysisData } from "./TokenAnalysisWidget";
+
diff --git a/surfsense_web/app/dashboard/[search_space_id]/crypto-tools-demo/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/crypto-tools-demo/page.tsx
new file mode 100644
index 000000000..4d25cd824
--- /dev/null
+++ b/surfsense_web/app/dashboard/[search_space_id]/crypto-tools-demo/page.tsx
@@ -0,0 +1,770 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { Shield, TrendingUp, TrendingDown, Users, AlertTriangle, Star, Bell, ExternalLink, Trash2, Plus, Activity, Zap, CheckCircle, Eye, Settings, Edit2, Percent, DollarSign, X, Flame, Fish, ArrowUpRight, ArrowDownRight, Globe, Wallet, User as UserIcon, Target } 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 { Switch } from "@/components/ui/switch";
+import { ChainIcon } from "@/components/crypto/ChainIcon";
+import { SafetyBadge } from "@/components/crypto/SafetyBadge";
+
+// ============ MOCK DATA ============
+const MOCK_TOKEN_ANALYSIS = {
+ symbol: "BULLA",
+ name: "Bulla Token",
+ chain: "solana",
+ contractAddress: "BULLAxK9xGJxGqPwPqTbGpLd9yKthvfNUet9V8wj8rWD",
+ price: 0.00001234,
+ priceChange24h: 156.7,
+ marketCap: 2100000,
+ volume24h: 1200000,
+ liquidity: 450000,
+ safetyScore: 72,
+ holderCount: 12500,
+ top10HolderPercent: 45,
+};
+
+const MOCK_WATCHLIST = [
+ { id: "1", symbol: "BULLA", name: "Bulla Token", chain: "solana", price: 0.00001234, priceChange24h: 156.7, alertCount: 2 },
+ { id: "2", symbol: "SOL", name: "Solana", chain: "solana", price: 98.45, priceChange24h: 3.2, alertCount: 1 },
+ { id: "3", symbol: "BONK", name: "Bonk", chain: "solana", price: 0.00002156, priceChange24h: -12.5, alertCount: 0 },
+ { id: "4", symbol: "WIF", name: "dogwifhat", chain: "solana", price: 2.34, priceChange24h: -5.8, alertCount: 3 },
+];
+
+const MOCK_ALERTS = [
+ { id: "1", type: "price_above" as const, value: 0.00002, enabled: true },
+ { id: "2", type: "price_below" as const, value: 0.000008, enabled: true },
+ { id: "3", type: "percent_change" as const, value: 20, enabled: false },
+ { id: "4", type: "whale_activity" as const, value: 50000, enabled: true },
+];
+
+const MOCK_PROACTIVE_ALERT = {
+ alertType: "price_surge" as const,
+ tokenSymbol: "BULLA",
+ tokenName: "Bulla Token",
+ value: 0.00001234,
+ previousValue: 0.00000482,
+ message: "BULLA just surged 156% in the last hour! This is unusual activity - consider taking profits or setting a stop-loss.",
+ severity: "warning" as const,
+ timestamp: "2 min ago",
+};
+
+const MOCK_ACTION_CONFIRMATION = {
+ actionType: "watchlist_add" as const,
+ tokenSymbol: "BULLA",
+ details: ["Price alerts (Β±10%)", "Whale activity monitoring", "Safety score changes"],
+};
+
+// ============ HELPER FUNCTIONS ============
+const formatPrice = (price: number): string => {
+ if (price < 0.00001) return `$${price.toExponential(2)}`;
+ if (price < 1) return `$${price.toFixed(6)}`;
+ return `$${price.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
+};
+
+const formatLargeNumber = (num: number): string => {
+ 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)}`;
+};
+
+// ============ DEMO COMPONENTS ============
+
+// 1. Token Analysis Demo
+function TokenAnalysisDemo() {
+ const args = MOCK_TOKEN_ANALYSIS;
+ return (
+
+
+
+ π
+ Token Analysis
+
+
+
+
+
+
+
+
+ {args.symbol}
+ {args.name}
+
+
+ {formatPrice(args.price)}
+ = 0 ? "text-green-500" : "text-red-500")}>
+ {args.priceChange24h >= 0 ? : }
+ {args.priceChange24h >= 0 ? "+" : ""}{args.priceChange24h.toFixed(2)}%
+
+
+
+
+
+
+
+
+
Market Cap
+
{formatLargeNumber(args.marketCap)}
+
+
+
24h Volume
+
{formatLargeNumber(args.volume24h)}
+
+
+
Liquidity
+
{formatLargeNumber(args.liquidity)}
+
+
+
Holders
+
+
+ {args.holderCount.toLocaleString()}
+
+
+
+ {args.top10HolderPercent > 50 && (
+
+
+
Top 10 holders own {args.top10HolderPercent}% of supply - high concentration risk
+
+ )}
+
+
+ Add to Watchlist
+
+
+
+
+
+
+ );
+}
+
+// 2. Watchlist Display Demo
+function WatchlistDisplayDemo() {
+ const tokens = MOCK_WATCHLIST;
+ const sortedByChange = [...tokens].sort((a, b) => b.priceChange24h - a.priceChange24h);
+ const bestPerformer = sortedByChange[0];
+ const worstPerformer = sortedByChange[sortedByChange.length - 1];
+
+ return (
+
+
+
+
+
+ Your Watchlist
+ {tokens.length}
+
+ Add Token
+
+
+
+
+ {tokens.map((token) => (
+
+
+
+
+
+ {token.symbol}
+ {token.alertCount > 0 && (
+
+ {token.alertCount}
+
+ )}
+
+
{token.name}
+
+
+
+
+
{formatPrice(token.price)}
+
= 0 ? "text-green-500" : "text-red-500")}>
+ {token.priceChange24h >= 0 ? : }
+ {token.priceChange24h >= 0 ? "+" : ""}{token.priceChange24h.toFixed(1)}%
+
+
+
+
+
+
+
+ ))}
+
+
+ {bestPerformer.symbol} is your best performer (+{bestPerformer.priceChange24h.toFixed(1)}%)
+ {worstPerformer.priceChange24h < 0 && (
+ β’ {worstPerformer.symbol} needs attention ({worstPerformer.priceChange24h.toFixed(1)}%)
+ )}
+
+
+
+ );
+}
+
+// 3. Action Confirmation Demo
+function ActionConfirmationDemo() {
+ const args = MOCK_ACTION_CONFIRMATION;
+ return (
+
+
+
+
+
+
+
+
+
+ Added to Watchlist
+ {args.tokenSymbol}
+
+
+
Default monitoring enabled:
+
+ {args.details.map((detail, i) => ({detail} ))}
+
+
+
+
+
+ View Watchlist
+ Edit Alerts
+
+
+
+ );
+}
+
+// 4. Alert Configuration Demo
+const ALERT_TYPE_CONFIG = {
+ price_above: { icon: TrendingUp, label: "Price Above", color: "text-green-500" },
+ price_below: { icon: TrendingDown, label: "Price Below", color: "text-red-500" },
+ percent_change: { icon: Percent, label: "% Change", color: "text-blue-500" },
+ volume_spike: { icon: Activity, label: "Volume Spike", color: "text-purple-500" },
+ whale_activity: { icon: DollarSign, label: "Whale Activity", color: "text-orange-500" },
+};
+
+const formatAlertValue = (type: string, value: number): string => {
+ if (type === "percent_change") return `${value > 0 ? "+" : ""}${value}%`;
+ if (type === "volume_spike") return `${value}x normal`;
+ if (type === "whale_activity") return `>${value.toLocaleString()} USD`;
+ return `$${value < 1 ? value.toFixed(6) : value.toLocaleString()}`;
+};
+
+function AlertConfigurationDemo() {
+ const alerts = MOCK_ALERTS;
+ const enabledCount = alerts.filter(a => a.enabled).length;
+
+ return (
+
+
+
+
+
+ Alerts for BULLA
+ {enabledCount} active
+
+ Add Alert
+
+
+
+
+ {alerts.map((alert) => {
+ const config = ALERT_TYPE_CONFIG[alert.type];
+ const Icon = config.icon;
+ return (
+
+
+
+
+
{config.label}
+
{formatAlertValue(alert.type, alert.value)}
+
+
+
+
+
+
+
+
+ );
+ })}
+
+
+
+ );
+}
+
+// 5. Proactive Alert Demo
+function ProactiveAlertDemo() {
+ const args = MOCK_PROACTIVE_ALERT;
+ const change = ((args.value - args.previousValue) / args.previousValue) * 100;
+
+ return (
+
+
+
+
+
+
+
+
+ PRICE SURGE
+ {args.tokenSymbol}
+ +{change.toFixed(1)}%
+ {args.timestamp}
+
+
{args.message}
+
+
+
+
+ View Details
+ Adjust Alert
+
+
+
+ );
+}
+
+// 6. Trending Tokens Demo
+const MOCK_TRENDING = [
+ { symbol: "BULLA", name: "Bulla Token", chain: "solana", price: 0.00001234, priceChange24h: 156.7, volume24h: 1200000, rank: 1 },
+ { symbol: "POPCAT", name: "Popcat", chain: "solana", price: 0.89, priceChange24h: 45.2, volume24h: 8500000, rank: 2 },
+ { symbol: "WIF", name: "dogwifhat", chain: "solana", price: 2.34, priceChange24h: 32.1, volume24h: 15000000, rank: 3 },
+ { symbol: "BONK", name: "Bonk", chain: "solana", price: 0.00002156, priceChange24h: 18.5, volume24h: 5200000, rank: 4 },
+];
+
+function TrendingTokensDemo() {
+ return (
+
+
+
+
+ Trending on Solana
+ 24h
+
+
+
+
+ {MOCK_TRENDING.map((token) => (
+
+
+
#{token.rank}
+
+
+
{token.symbol}
+
{token.name}
+
+
+
+
+
{formatPrice(token.price)}
+
= 0 ? "text-green-500" : "text-red-500")}>
+ +{token.priceChange24h.toFixed(1)}%
+
+
+
+
Volume
+
{formatLargeNumber(token.volume24h)}
+
+
+
+
+ ))}
+
+
+
+ );
+}
+
+// 7. Whale Activity Demo
+const MOCK_WHALE_TXS = [
+ { id: "1", type: "buy" as const, amountUsd: 250000, walletLabel: "Smart Money 1", timestamp: "5m ago" },
+ { id: "2", type: "sell" as const, amountUsd: 180000, walletLabel: "Whale #42", timestamp: "12m ago" },
+ { id: "3", type: "buy" as const, amountUsd: 320000, walletLabel: null, timestamp: "25m ago" },
+ { id: "4", type: "transfer" as const, amountUsd: 500000, walletLabel: "Exchange Hot Wallet", timestamp: "1h ago" },
+];
+
+function WhaleActivityDemo() {
+ const summary = { totalBuyVolume: 570000, totalSellVolume: 180000, netFlow: 390000, uniqueWhales: 4 };
+ return (
+
+
+
+
+
+ Whale Activity - BULLA
+
+
+
+
+
+
+
+
Buy Volume
+
{formatLargeNumber(summary.totalBuyVolume)}
+
+
+
Sell Volume
+
{formatLargeNumber(summary.totalSellVolume)}
+
+
+
Net Flow
+
+{formatLargeNumber(summary.netFlow)}
+
+
+
Unique Whales
+
{summary.uniqueWhales}
+
+
+
+ {MOCK_WHALE_TXS.map((tx) => (
+
+
+
+ {tx.type === "buy" ?
: tx.type === "sell" ?
:
}
+
+
+
{tx.type}
+
{formatLargeNumber(tx.amountUsd)}
+
{tx.walletLabel || "Unknown Wallet"} β’ {tx.timestamp}
+
+
+
+ ))}
+
+
+
+ );
+}
+
+// 8. Market Overview Demo
+const MOCK_MARKET = [
+ { symbol: "BTC", name: "Bitcoin", price: 67500, priceChange24h: 2.3 },
+ { symbol: "ETH", name: "Ethereum", price: 3450, priceChange24h: -1.2 },
+ { symbol: "SOL", name: "Solana", price: 98.45, priceChange24h: 5.7 },
+];
+
+function MarketOverviewDemo() {
+ return (
+
+
+
+
+ Market Overview
+
+
+
+
+
+
Total Market Cap
+
$2.45T
+
+
+
+
BTC Dominance
+
52.3%
+
+
+
Fear & Greed
+
72 - Greed
+
+
+
+ {MOCK_MARKET.map((token) => (
+
+
+
{token.symbol}
+
{token.name}
+
+
+
${token.price.toLocaleString()}
+
= 0 ? "text-green-500" : "text-red-500")}>
+ {token.priceChange24h >= 0 ? "+" : ""}{token.priceChange24h.toFixed(2)}%
+
+
+
+ ))}
+
+
+
+ );
+}
+
+// 9. Holder Analysis Demo
+const MOCK_HOLDERS = [
+ { rank: 1, address: "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1", label: "Raydium LP", percentage: 15.2, balance: 152000000 },
+ { rank: 2, address: "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", label: null, percentage: 8.5, balance: 85000000 },
+ { rank: 3, address: "HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH", label: "Team Wallet", percentage: 7.2, balance: 72000000 },
+];
+
+function HolderAnalysisDemo() {
+ return (
+
+
+
+
+
+ Holder Analysis - BULLA
+
+
+
+
+
+
+
+
Total Holders
+
12,500
+
+
+
+
+
Concentration Risk
+
Medium
+
+
+
+ {MOCK_HOLDERS.map((holder) => (
+
+
+
#{holder.rank}
+
+
{holder.label || `${holder.address.slice(0, 6)}...${holder.address.slice(-4)}`}
+
+
+
+
{holder.percentage.toFixed(2)}%
+
{(holder.balance / 1e6).toFixed(1)}M
+
+
+ ))}
+
+
+
+ );
+}
+
+// 10. Portfolio Display Demo
+const MOCK_PORTFOLIO_HOLDINGS = [
+ { symbol: "SOL", name: "Solana", chain: "solana", balance: 50, value: 4922.5, pnlPercent: 125.5, allocation: 45 },
+ { symbol: "BULLA", name: "Bulla Token", chain: "solana", balance: 5000000, value: 61.7, pnlPercent: 256.7, allocation: 5.6 },
+ { symbol: "ETH", name: "Ethereum", chain: "ethereum", balance: 1.5, value: 5175, pnlPercent: 45.2, allocation: 47.3 },
+];
+
+function PortfolioDisplayDemo() {
+ return (
+
+
+
+
+ Your Portfolio
+
+
+
+
+
Total Value
+
$10,934.20
+
+ +$3,245.80 (+42.3%)
+
+
+
+ {MOCK_PORTFOLIO_HOLDINGS.map((holding) => (
+
+
+
+
+
+ {holding.symbol}
+ {holding.allocation.toFixed(1)}%
+
+
{holding.balance.toLocaleString()} tokens
+
+
+
+
${holding.value.toLocaleString()}
+
= 0 ? "text-green-500" : "text-red-500")}>
+ +{holding.pnlPercent.toFixed(2)}%
+
+
+
+ ))}
+
+
+
+ );
+}
+
+// 11. User Profile Demo
+function UserProfileDemo() {
+ return (
+
+
+
+
+ Your Investment Profile
+
+
+
+
+
+
+
+ Risk Tolerance
+
+
Moderate
+
Balance between risk and reward
+
+
+
+
+ Investment Style
+
+
Swing Trader
+
Hold for days to weeks
+
+
+
+
Preferred Chains
+
+ Solana
+ Ethereum
+ Base
+
+
+
+ Say "update my risk tolerance to aggressive" to change settings
+
+
+
+ );
+}
+
+export default function CryptoToolsDemoPage() {
+ return (
+
+
+
π§ͺ Crypto Tools Demo
+
Preview of all crypto tool UI components with mock data
+
These components render inline in chat when AI calls the corresponding tools.
+
+
+
+
+ 1
+ Token Analysis analyze_token
+
+ Triggered by: "Analyze BULLA", "Is BULLA safe?", "Research this token"
+
+
+
+
+ 2
+ Watchlist Display show_watchlist
+
+ Triggered by: "Show my watchlist", "What tokens am I tracking?"
+
+
+
+
+ 3
+ Action Confirmation confirm_action
+
+ Triggered by: "Add BULLA to watchlist", "Remove SOL from watchlist"
+
+
+
+
+ 4
+ Alert Configuration configure_alerts
+
+ Triggered by: "Show my alerts for BULLA", "Set alert if BULLA drops 20%"
+
+
+
+
+ 5
+ Proactive Alert proactive_alert
+
+ AI-initiated: Automatically sent when price surges, whale activity detected, etc.
+
+
+
+
+ 6
+ Trending Tokens get_trending_tokens
+
+ Triggered by: "What's hot on Solana?", "Show trending tokens", "What's pumping today?"
+
+
+
+
+ 7
+ Whale Activity get_whale_activity
+
+ Triggered by: "Show whale activity for BULLA", "Any big buys?", "Who's accumulating?"
+
+
+
+
+ 8
+ Market Overview get_market_overview
+
+ Triggered by: "How's the market?", "Show market overview", "What's the sentiment?"
+
+
+
+
+ 9
+ Holder Analysis analyze_holders
+
+ Triggered by: "Who holds BULLA?", "Show holder distribution", "Is it concentrated?"
+
+
+
+
+ 10
+ Portfolio Display get_portfolio
+
+ Triggered by: "How's my portfolio?", "Show my holdings", "What's my P&L?"
+
+
+
+
+ 11
+ User Profile get_user_profile
+
+ Triggered by: "Show my profile", "What's my risk tolerance?", "Update my investment style"
+
+
+
+
+
π‘ How it works
+
+ When you chat with the AI and ask crypto-related questions, the AI calls these tools and the corresponding UI components render inline in the chat.
+ This creates a seamless conversational experience where data and actions are embedded directly in the conversation.
+
+
+
+
β
All 11 Tool-UI Components Complete
+
+ Components 1-5 (blue) are the original tools. Components 6-11 (orange) are newly added to cover all crypto features in the spec.
+
+
+
+ );
+}
+
diff --git a/surfsense_web/app/dashboard/[search_space_id]/crypto/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/crypto/page.tsx
new file mode 100644
index 000000000..5db08d2dc
--- /dev/null
+++ b/surfsense_web/app/dashboard/[search_space_id]/crypto/page.tsx
@@ -0,0 +1,272 @@
+"use client";
+
+import { useState } from "react";
+import { useParams, useRouter } from "next/navigation";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { MessageSquare, Sparkles, Star, Bell, TrendingUp, ArrowRight, X, Plus, User } from "lucide-react";
+import {
+ MarketOverview,
+ WatchlistTable,
+ AlertsPanel,
+ PortfolioSummary,
+ AddTokenModal,
+ CreateAlertModal,
+ UserProfileSection,
+ type AlertConfig,
+ type UserProfile,
+} from "@/components/crypto";
+import {
+ MOCK_MARKET_PRICES,
+ MOCK_WATCHLIST,
+ MOCK_ALERTS,
+ MOCK_PORTFOLIO,
+} from "@/lib/mock/cryptoMockData";
+
+// Default user profile
+const DEFAULT_PROFILE: UserProfile = {
+ riskTolerance: "moderate",
+ investmentStyle: "swing",
+ preferredChains: ["solana", "ethereum"],
+ notifications: {
+ priceAlerts: true,
+ whaleAlerts: true,
+ newsAlerts: false,
+ },
+};
+
+/**
+ * Crypto Dashboard Page
+ *
+ * Full-featured crypto management dashboard with:
+ * - Market Overview
+ * - Watchlist Management (with Add Token modal)
+ * - Alerts Management (with Create Alert modal)
+ * - Portfolio Summary
+ * - User Profile Settings
+ *
+ * Also includes a banner promoting the AI Chat for research & analysis.
+ */
+export default function CryptoDashboardPage() {
+ const router = useRouter();
+ const params = useParams();
+ const searchSpaceId = params.search_space_id;
+
+ // UI State
+ const [showAIBanner, setShowAIBanner] = useState(true);
+ const [activeTab, setActiveTab] = useState("watchlist");
+ const [showAddTokenModal, setShowAddTokenModal] = useState(false);
+ const [showCreateAlertModal, setShowCreateAlertModal] = useState(false);
+ const [alertPrefilledToken, setAlertPrefilledToken] = useState<{ symbol: string; chain: string } | undefined>();
+
+ // Data State (mock - would be from API in production)
+ const [watchlist, setWatchlist] = useState(MOCK_WATCHLIST);
+ const [alerts, setAlerts] = useState(MOCK_ALERTS);
+ const [userProfile, setUserProfile] = useState
(DEFAULT_PROFILE);
+
+ const handleGoToChat = () => {
+ router.push(`/dashboard/${searchSpaceId}/new-chat`);
+ };
+
+ const handleTokenClick = (token: any) => {
+ router.push(`/dashboard/${searchSpaceId}/new-chat?query=Analyze ${token.symbol}`);
+ };
+
+ const handleConfigureAlerts = (token: any) => {
+ setAlertPrefilledToken({ symbol: token.symbol, chain: token.chain });
+ setShowCreateAlertModal(true);
+ };
+
+ const handleAlertClick = (alert: any) => {
+ router.push(`/dashboard/${searchSpaceId}/new-chat?query=Tell me about ${alert.tokenSymbol}`);
+ };
+
+ const handleRemoveToken = (tokenId: string) => {
+ setWatchlist((prev) => prev.filter((t) => t.id !== tokenId));
+ };
+
+ const handleAddToken = (token: { symbol: string; name: string; chain: string; contractAddress?: string }) => {
+ const newToken = {
+ id: `token-${Date.now()}`,
+ symbol: token.symbol,
+ name: token.name,
+ chain: token.chain,
+ contractAddress: token.contractAddress,
+ price: 0,
+ priceChange24h: 0,
+ safetyScore: undefined,
+ alertCount: 0,
+ };
+ setWatchlist((prev) => [...prev, newToken]);
+ };
+
+ const handleCreateAlert = (alertConfig: AlertConfig) => {
+ const newAlert = {
+ id: `alert-${Date.now()}`,
+ tokenSymbol: alertConfig.tokenSymbol,
+ chain: alertConfig.chain,
+ type: alertConfig.alertType,
+ message: `${alertConfig.alertType.replace("_", " ")} alert for ${alertConfig.tokenSymbol}`,
+ severity: "info" as const,
+ timestamp: new Date().toISOString(),
+ isRead: false,
+ };
+ setAlerts((prev) => [newAlert, ...prev]);
+ };
+
+ const handleSaveProfile = (profile: UserProfile) => {
+ setUserProfile(profile);
+ // In production, save to backend
+ };
+
+ return (
+
+ {/* AI Chat Promotion Banner */}
+ {showAIBanner && (
+
+
+
+
+
+
+
+
+ π‘ Try our AI Crypto Advisor for deeper analysis!
+
+
+ Ask questions like "Is BULLA safe?" or "Set alert if SOL drops 10%"
+
+
+
+
+
+
+ Open AI Chat
+
+
+
setShowAIBanner(false)}>
+
+
+
+
+
+ )}
+
+ {/* Main Content */}
+
+
+
+ π Crypto Dashboard
+
+
+ Manage your watchlist, alerts, and track market trends
+
+
+
+
+
+
+
+ Watchlist
+
+
+
+ Alerts
+
+
+
+ Market
+
+
+
+ Profile
+
+
+
+ {/* Watchlist Tab */}
+
+
+
setShowAddTokenModal(true)} className="gap-2">
+
+ Add Token
+
+
+
+
+
+ {/* Alerts Tab */}
+
+
+
{ setAlertPrefilledToken(undefined); setShowCreateAlertModal(true); }} className="gap-2">
+
+ Create Alert
+
+
+
+
+
+ {/* Market Tab */}
+
+
+
+
+
+
+
+
Want deeper market insights?
+
+ Ask our AI about trending tokens, market sentiment, or specific analysis
+
+
+
+ Ask AI Advisor
+
+
+
+
+
+
+
+ {/* Profile Tab */}
+
+
+
+
+
+
+
+
+ {/* Modals */}
+
+
+
+ );
+}
diff --git a/surfsense_web/components/crypto/AddTokenModal.tsx b/surfsense_web/components/crypto/AddTokenModal.tsx
new file mode 100644
index 000000000..6931f435c
--- /dev/null
+++ b/surfsense_web/components/crypto/AddTokenModal.tsx
@@ -0,0 +1,150 @@
+"use client";
+
+import { useState } from "react";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Plus, Search, Loader2 } from "lucide-react";
+
+interface AddTokenModalProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onAddToken: (token: { symbol: string; name: string; chain: string; contractAddress?: string }) => void;
+}
+
+const SUPPORTED_CHAINS = [
+ { value: "solana", label: "Solana" },
+ { value: "ethereum", label: "Ethereum" },
+ { value: "base", label: "Base" },
+ { value: "arbitrum", label: "Arbitrum" },
+ { value: "polygon", label: "Polygon" },
+];
+
+export function AddTokenModal({ open, onOpenChange, onAddToken }: AddTokenModalProps) {
+ const [symbol, setSymbol] = useState("");
+ const [name, setName] = useState("");
+ const [chain, setChain] = useState("solana");
+ const [contractAddress, setContractAddress] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState("");
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError("");
+
+ if (!symbol.trim()) {
+ setError("Token symbol is required");
+ return;
+ }
+
+ if (!chain) {
+ setError("Please select a chain");
+ return;
+ }
+
+ setIsLoading(true);
+
+ // Simulate API call delay
+ await new Promise((resolve) => setTimeout(resolve, 500));
+
+ onAddToken({
+ symbol: symbol.toUpperCase().trim(),
+ name: name.trim() || symbol.toUpperCase().trim(),
+ chain,
+ contractAddress: contractAddress.trim() || undefined,
+ });
+
+ // Reset form
+ setSymbol("");
+ setName("");
+ setChain("solana");
+ setContractAddress("");
+ setIsLoading(false);
+ onOpenChange(false);
+ };
+
+ return (
+
+
+
+
+
+ Add Token to Watchlist
+
+
+
+
+
+ );
+}
+
diff --git a/surfsense_web/components/crypto/AlertsPanel.tsx b/surfsense_web/components/crypto/AlertsPanel.tsx
new file mode 100644
index 000000000..fd675fc00
--- /dev/null
+++ b/surfsense_web/components/crypto/AlertsPanel.tsx
@@ -0,0 +1,171 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { Bell, BellOff, Check, AlertTriangle, Info, XCircle } from "lucide-react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { ChainIcon } from "./ChainIcon";
+import type { Alert } from "@/lib/mock/cryptoMockData";
+
+interface AlertsPanelProps {
+ alerts: Alert[];
+ onAlertClick?: (alert: Alert) => void;
+ onMarkAsRead?: (alertId: string) => void;
+ onMarkAllAsRead?: () => void;
+ onDismiss?: (alertId: string) => void;
+ className?: string;
+}
+
+function formatTimeAgo(date: Date): string {
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
+ if (seconds < 60) return "just now";
+ const minutes = Math.floor(seconds / 60);
+ if (minutes < 60) return `${minutes}m ago`;
+ const hours = Math.floor(minutes / 60);
+ if (hours < 24) return `${hours}h ago`;
+ const days = Math.floor(hours / 24);
+ return `${days}d ago`;
+}
+
+function getSeverityConfig(severity: Alert["severity"]) {
+ switch (severity) {
+ case "critical":
+ return {
+ icon: XCircle,
+ color: "text-red-500",
+ bg: "bg-red-500/10",
+ border: "border-red-500/20",
+ };
+ case "warning":
+ return {
+ icon: AlertTriangle,
+ color: "text-yellow-500",
+ bg: "bg-yellow-500/10",
+ border: "border-yellow-500/20",
+ };
+ default:
+ return {
+ icon: Info,
+ color: "text-blue-500",
+ bg: "bg-blue-500/10",
+ border: "border-blue-500/20",
+ };
+ }
+}
+
+function AlertItem({
+ alert,
+ onClick,
+ onMarkAsRead,
+ onDismiss,
+}: {
+ alert: Alert;
+ onClick?: () => void;
+ onMarkAsRead?: () => void;
+ onDismiss?: () => void;
+}) {
+ const config = getSeverityConfig(alert.severity);
+ const Icon = config.icon;
+
+ return (
+
+
+
+
+
+
+
+ {alert.tokenSymbol}
+ {!alert.isRead && (
+ NEW
+ )}
+
+
{alert.message}
+
{formatTimeAgo(alert.timestamp)}
+
+
+ {!alert.isRead && (
+ {
+ e.stopPropagation();
+ onMarkAsRead?.();
+ }}
+ title="Mark as read"
+ >
+
+
+ )}
+
+
+ );
+}
+
+export function AlertsPanel({
+ alerts,
+ onAlertClick,
+ onMarkAsRead,
+ onMarkAllAsRead,
+ onDismiss,
+ className,
+}: AlertsPanelProps) {
+ const unreadCount = alerts.filter((a) => !a.isRead).length;
+
+ return (
+
+
+
+
+ Alerts
+ {unreadCount > 0 && (
+ {unreadCount}
+ )}
+
+ {unreadCount > 0 && (
+
+
+ Mark all read
+
+ )}
+
+
+
+ {alerts.length === 0 ? (
+
+
+
No alerts yet
+
Configure alerts on your watchlist tokens
+
+ ) : (
+
+
+ {alerts.map((alert) => (
+
onAlertClick?.(alert)}
+ onMarkAsRead={() => onMarkAsRead?.(alert.id)}
+ onDismiss={() => onDismiss?.(alert.id)}
+ />
+ ))}
+
+
+ )}
+
+
+ );
+}
+
diff --git a/surfsense_web/components/crypto/ChainIcon.tsx b/surfsense_web/components/crypto/ChainIcon.tsx
new file mode 100644
index 000000000..f2d9e111d
--- /dev/null
+++ b/surfsense_web/components/crypto/ChainIcon.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import type { ChainType } from "@/lib/mock/cryptoMockData";
+
+interface ChainIconProps {
+ chain: ChainType;
+ size?: "sm" | "md" | "lg";
+ showName?: boolean;
+ className?: string;
+}
+
+const chainConfig: Record = {
+ solana: { color: "#9945FF", icon: "β", name: "Solana" },
+ ethereum: { color: "#627EEA", icon: "Ξ", name: "Ethereum" },
+ base: { color: "#0052FF", icon: "π΅", name: "Base" },
+ arbitrum: { color: "#28A0F0", icon: "π·", name: "Arbitrum" },
+ polygon: { color: "#8247E5", icon: "⬑", name: "Polygon" },
+ bsc: { color: "#F0B90B", icon: "β¬’", name: "BNB Chain" },
+};
+
+const sizeClasses = {
+ sm: "h-4 w-4 text-xs",
+ md: "h-5 w-5 text-sm",
+ lg: "h-6 w-6 text-base",
+};
+
+export function ChainIcon({ chain, size = "md", showName = false, className }: ChainIconProps) {
+ const config = chainConfig[chain] || { color: "#888888", icon: "?", name: chain };
+
+ return (
+
+
+ {config.icon}
+
+ {showName && (
+ {config.name}
+ )}
+
+ );
+}
+
diff --git a/surfsense_web/components/crypto/CreateAlertModal.tsx b/surfsense_web/components/crypto/CreateAlertModal.tsx
new file mode 100644
index 000000000..517d942a8
--- /dev/null
+++ b/surfsense_web/components/crypto/CreateAlertModal.tsx
@@ -0,0 +1,166 @@
+"use client";
+
+import { useState } from "react";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Switch } from "@/components/ui/switch";
+import { Bell, Loader2 } from "lucide-react";
+
+interface CreateAlertModalProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onCreateAlert: (alert: AlertConfig) => void;
+ prefilledToken?: { symbol: string; chain: string };
+}
+
+export interface AlertConfig {
+ tokenSymbol: string;
+ chain: string;
+ alertType: string;
+ threshold?: number;
+ enabled: boolean;
+}
+
+const ALERT_TYPES = [
+ { value: "price_above", label: "Price Above", hasThreshold: true, unit: "$" },
+ { value: "price_below", label: "Price Below", hasThreshold: true, unit: "$" },
+ { value: "price_change", label: "Price Change %", hasThreshold: true, unit: "%" },
+ { value: "volume_spike", label: "Volume Spike", hasThreshold: true, unit: "x" },
+ { value: "whale_buy", label: "Whale Buy", hasThreshold: false },
+ { value: "whale_sell", label: "Whale Sell", hasThreshold: false },
+];
+
+const SUPPORTED_CHAINS = [
+ { value: "solana", label: "Solana" },
+ { value: "ethereum", label: "Ethereum" },
+ { value: "base", label: "Base" },
+];
+
+export function CreateAlertModal({ open, onOpenChange, onCreateAlert, prefilledToken }: CreateAlertModalProps) {
+ const [tokenSymbol, setTokenSymbol] = useState(prefilledToken?.symbol || "");
+ const [chain, setChain] = useState(prefilledToken?.chain || "solana");
+ const [alertType, setAlertType] = useState("price_above");
+ const [threshold, setThreshold] = useState("");
+ const [enabled, setEnabled] = useState(true);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState("");
+
+ const selectedAlertType = ALERT_TYPES.find((t) => t.value === alertType);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError("");
+
+ if (!tokenSymbol.trim()) {
+ setError("Token symbol is required");
+ return;
+ }
+
+ if (selectedAlertType?.hasThreshold && !threshold) {
+ setError("Threshold value is required for this alert type");
+ return;
+ }
+
+ setIsLoading(true);
+ await new Promise((resolve) => setTimeout(resolve, 500));
+
+ onCreateAlert({
+ tokenSymbol: tokenSymbol.toUpperCase().trim(),
+ chain,
+ alertType,
+ threshold: selectedAlertType?.hasThreshold ? parseFloat(threshold) : undefined,
+ enabled,
+ });
+
+ // Reset form
+ setTokenSymbol("");
+ setAlertType("price_above");
+ setThreshold("");
+ setEnabled(true);
+ setIsLoading(false);
+ onOpenChange(false);
+ };
+
+ return (
+
+
+
+
+
+ Create Alert
+
+
+
+
+
+ );
+}
+
diff --git a/surfsense_web/components/crypto/MarketOverview.tsx b/surfsense_web/components/crypto/MarketOverview.tsx
new file mode 100644
index 000000000..3f0a9930b
--- /dev/null
+++ b/surfsense_web/components/crypto/MarketOverview.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { TrendingUp, TrendingDown } from "lucide-react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import type { TokenPrice } from "@/lib/mock/cryptoMockData";
+import { formatPrice, formatPercent, formatLargeNumber } from "@/lib/mock/cryptoMockData";
+
+interface MarketOverviewProps {
+ tokens: TokenPrice[];
+ className?: string;
+}
+
+function MarketCard({ token }: { token: TokenPrice }) {
+ const isPositive = token.priceChange24h > 0;
+ const isNegative = token.priceChange24h < 0;
+
+ return (
+
+
+
+ {token.icon || token.symbol.charAt(0)}
+
+
+
{token.symbol}
+
{token.name}
+
+
+
+
{formatPrice(token.price)}
+
+ {isPositive && }
+ {isNegative && }
+ {formatPercent(token.priceChange24h)}
+
+
+
+ );
+}
+
+export function MarketOverview({ tokens, className }: MarketOverviewProps) {
+ return (
+
+
+
+ π Market Overview
+
+
+
+ {tokens.map((token) => (
+
+ ))}
+
+
+ );
+}
+
diff --git a/surfsense_web/components/crypto/PortfolioSummary.tsx b/surfsense_web/components/crypto/PortfolioSummary.tsx
new file mode 100644
index 000000000..430d7be3d
--- /dev/null
+++ b/surfsense_web/components/crypto/PortfolioSummary.tsx
@@ -0,0 +1,133 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { TrendingUp, TrendingDown, Wallet, PieChart } from "lucide-react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { ChainIcon } from "./ChainIcon";
+import type { PortfolioSummary as PortfolioSummaryType, PortfolioToken } from "@/lib/mock/cryptoMockData";
+import { formatPrice, formatPercent, formatLargeNumber } from "@/lib/mock/cryptoMockData";
+
+interface PortfolioSummaryProps {
+ portfolio: PortfolioSummaryType;
+ className?: string;
+}
+
+function StatCard({
+ label,
+ value,
+ change,
+ changePercent,
+}: {
+ label: string;
+ value: string;
+ change?: number;
+ changePercent?: number;
+}) {
+ const isPositive = change !== undefined && change > 0;
+ const isNegative = change !== undefined && change < 0;
+
+ return (
+
+
{label}
+
{value}
+ {change !== undefined && changePercent !== undefined && (
+
+ {isPositive && }
+ {isNegative && }
+ {formatPrice(Math.abs(change))}
+ ({formatPercent(changePercent)})
+
+ )}
+
+ );
+}
+
+function TokenRow({ token }: { token: PortfolioToken }) {
+ const isPositive = token.pnl > 0;
+ const isNegative = token.pnl < 0;
+
+ return (
+
+
+
+
+
{token.symbol}
+
+ {token.amount.toLocaleString()} tokens
+
+
+
+
+
{formatPrice(token.value)}
+
+ {formatPercent(token.pnlPercent)}
+
+
+
+
{token.allocation.toFixed(1)}%
+
+
+
+ );
+}
+
+export function PortfolioSummary({ portfolio, className }: PortfolioSummaryProps) {
+ return (
+
+
+
+ Portfolio
+
+
+
+ {/* Summary Stats */}
+
+
+
+
+
+ {/* Token Holdings */}
+
+
+
+ {portfolio.tokens.map((token) => (
+
+ ))}
+
+
+
+
+ );
+}
+
diff --git a/surfsense_web/components/crypto/PriceDisplay.tsx b/surfsense_web/components/crypto/PriceDisplay.tsx
new file mode 100644
index 000000000..897e7e0e1
--- /dev/null
+++ b/surfsense_web/components/crypto/PriceDisplay.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { TrendingUp, TrendingDown, Minus } from "lucide-react";
+import { formatPrice, formatPercent } from "@/lib/mock/cryptoMockData";
+
+interface PriceDisplayProps {
+ price: number;
+ priceChange?: number;
+ size?: "sm" | "md" | "lg";
+ showIcon?: boolean;
+ className?: string;
+}
+
+const sizeClasses = {
+ sm: { price: "text-sm font-medium", change: "text-xs" },
+ md: { price: "text-lg font-semibold", change: "text-sm" },
+ lg: { price: "text-2xl font-bold", change: "text-base" },
+};
+
+export function PriceDisplay({
+ price,
+ priceChange,
+ size = "md",
+ showIcon = true,
+ className,
+}: PriceDisplayProps) {
+ const isPositive = priceChange !== undefined && priceChange > 0;
+ const isNegative = priceChange !== undefined && priceChange < 0;
+ const isNeutral = priceChange === undefined || priceChange === 0;
+
+ return (
+
+ {formatPrice(price)}
+ {priceChange !== undefined && (
+
+ {showIcon && (
+ <>
+ {isPositive && }
+ {isNegative && }
+ {isNeutral && }
+ >
+ )}
+ {formatPercent(priceChange)}
+
+ )}
+
+ );
+}
+
diff --git a/surfsense_web/components/crypto/SafetyBadge.tsx b/surfsense_web/components/crypto/SafetyBadge.tsx
new file mode 100644
index 000000000..c1fc60c63
--- /dev/null
+++ b/surfsense_web/components/crypto/SafetyBadge.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { Shield, ShieldAlert, ShieldCheck, ShieldX } from "lucide-react";
+import { getSafetyLabel } from "@/lib/mock/cryptoMockData";
+
+interface SafetyBadgeProps {
+ score: number;
+ size?: "sm" | "md" | "lg";
+ showScore?: boolean;
+ className?: string;
+}
+
+const sizeClasses = {
+ sm: { badge: "px-1.5 py-0.5 text-xs", icon: "h-3 w-3" },
+ md: { badge: "px-2 py-1 text-sm", icon: "h-4 w-4" },
+ lg: { badge: "px-3 py-1.5 text-base", icon: "h-5 w-5" },
+};
+
+function getScoreConfig(score: number) {
+ if (score >= 80) {
+ return {
+ color: "bg-green-500/10 text-green-600 border-green-500/20",
+ Icon: ShieldCheck,
+ };
+ }
+ if (score >= 60) {
+ return {
+ color: "bg-yellow-500/10 text-yellow-600 border-yellow-500/20",
+ Icon: Shield,
+ };
+ }
+ if (score >= 40) {
+ return {
+ color: "bg-orange-500/10 text-orange-600 border-orange-500/20",
+ Icon: ShieldAlert,
+ };
+ }
+ return {
+ color: "bg-red-500/10 text-red-600 border-red-500/20",
+ Icon: ShieldX,
+ };
+}
+
+export function SafetyBadge({ score, size = "md", showScore = true, className }: SafetyBadgeProps) {
+ const { color, Icon } = getScoreConfig(score);
+ const label = getSafetyLabel(score);
+
+ return (
+
+
+ {label}
+ {showScore && ({score}) }
+
+ );
+}
+
diff --git a/surfsense_web/components/crypto/UserProfileSection.tsx b/surfsense_web/components/crypto/UserProfileSection.tsx
new file mode 100644
index 000000000..97aa1634a
--- /dev/null
+++ b/surfsense_web/components/crypto/UserProfileSection.tsx
@@ -0,0 +1,165 @@
+"use client";
+
+import { useState } from "react";
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Switch } from "@/components/ui/switch";
+import { Badge } from "@/components/ui/badge";
+import { User, Shield, Target, Bell, Save, Loader2 } from "lucide-react";
+
+export interface UserProfile {
+ riskTolerance: "conservative" | "moderate" | "aggressive";
+ investmentStyle: "day_trader" | "swing" | "long_term";
+ preferredChains: string[];
+ notifications: {
+ priceAlerts: boolean;
+ whaleAlerts: boolean;
+ newsAlerts: boolean;
+ };
+}
+
+interface UserProfileSectionProps {
+ profile: UserProfile;
+ onSave: (profile: UserProfile) => void;
+}
+
+const CHAINS = ["solana", "ethereum", "base", "arbitrum", "polygon"];
+
+export function UserProfileSection({ profile: initialProfile, onSave }: UserProfileSectionProps) {
+ const [profile, setProfile] = useState(initialProfile);
+ const [isSaving, setIsSaving] = useState(false);
+ const [hasChanges, setHasChanges] = useState(false);
+
+ const updateProfile = (updates: Partial) => {
+ setProfile((prev) => ({ ...prev, ...updates }));
+ setHasChanges(true);
+ };
+
+ const toggleChain = (chain: string) => {
+ const newChains = profile.preferredChains.includes(chain)
+ ? profile.preferredChains.filter((c) => c !== chain)
+ : [...profile.preferredChains, chain];
+ updateProfile({ preferredChains: newChains });
+ };
+
+ const handleSave = async () => {
+ setIsSaving(true);
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ onSave(profile);
+ setIsSaving(false);
+ setHasChanges(false);
+ };
+
+ return (
+
+
+
+
+ Investment Profile
+
+
+ Configure your risk preferences and notification settings
+
+
+
+ {/* Risk Tolerance */}
+
+
+
+ Risk Tolerance
+
+ updateProfile({ riskTolerance: v as UserProfile["riskTolerance"] })}
+ >
+
+
+
+
+ Conservative - Lower risk, stable returns
+ Moderate - Balanced risk/reward
+ Aggressive - Higher risk, higher potential
+
+
+
+
+ {/* Investment Style */}
+
+
+
+ Investment Style
+
+ updateProfile({ investmentStyle: v as UserProfile["investmentStyle"] })}
+ >
+
+
+
+
+ Day Trader - Quick trades, high frequency
+ Swing Trader - Hold for days to weeks
+ Long Term - Hold for months to years
+
+
+
+
+ {/* Preferred Chains */}
+
+
Preferred Chains
+
+ {CHAINS.map((chain) => (
+ toggleChain(chain)}
+ >
+ {chain}
+
+ ))}
+
+
+
+ {/* Notifications */}
+
+
+
+ Notifications
+
+
+
+ Price Alerts
+ updateProfile({ notifications: { ...profile.notifications, priceAlerts: v } })}
+ />
+
+
+ Whale Activity Alerts
+ updateProfile({ notifications: { ...profile.notifications, whaleAlerts: v } })}
+ />
+
+
+ News & Updates
+ updateProfile({ notifications: { ...profile.notifications, newsAlerts: v } })}
+ />
+
+
+
+
+ {/* Save Button */}
+
+ {isSaving ? <> Saving...> : <> Save Profile>}
+
+
+
+ );
+}
+
diff --git a/surfsense_web/components/crypto/WatchlistTable.tsx b/surfsense_web/components/crypto/WatchlistTable.tsx
new file mode 100644
index 000000000..8a7f1b008
--- /dev/null
+++ b/surfsense_web/components/crypto/WatchlistTable.tsx
@@ -0,0 +1,211 @@
+"use client";
+
+import { useState } from "react";
+import { cn } from "@/lib/utils";
+import {
+ Star,
+ Bell,
+ ExternalLink,
+ MoreHorizontal,
+ ArrowUpDown,
+ Trash2,
+ Settings,
+} from "lucide-react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Badge } from "@/components/ui/badge";
+import { ChainIcon } from "./ChainIcon";
+import { SafetyBadge } from "./SafetyBadge";
+import type { WatchlistToken } from "@/lib/mock/cryptoMockData";
+import { formatPrice, formatPercent, formatLargeNumber } from "@/lib/mock/cryptoMockData";
+
+interface WatchlistTableProps {
+ tokens: WatchlistToken[];
+ onTokenClick?: (token: WatchlistToken) => void;
+ onRemoveToken?: (tokenId: string) => void;
+ onConfigureAlerts?: (token: WatchlistToken) => void;
+ className?: string;
+}
+
+type SortField = "symbol" | "price" | "priceChange24h" | "volume24h" | "marketCap" | "safetyScore";
+type SortDirection = "asc" | "desc";
+
+export function WatchlistTable({
+ tokens,
+ onTokenClick,
+ onRemoveToken,
+ onConfigureAlerts,
+ className,
+}: WatchlistTableProps) {
+ const [sortField, setSortField] = useState("priceChange24h");
+ const [sortDirection, setSortDirection] = useState("desc");
+
+ const handleSort = (field: SortField) => {
+ if (sortField === field) {
+ setSortDirection(sortDirection === "asc" ? "desc" : "asc");
+ } else {
+ setSortField(field);
+ setSortDirection("desc");
+ }
+ };
+
+ const sortedTokens = [...tokens].sort((a, b) => {
+ const aVal = a[sortField];
+ const bVal = b[sortField];
+ if (typeof aVal === "string" && typeof bVal === "string") {
+ return sortDirection === "asc" ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
+ }
+ return sortDirection === "asc"
+ ? (aVal as number) - (bVal as number)
+ : (bVal as number) - (aVal as number);
+ });
+
+ const SortableHeader = ({ field, children }: { field: SortField; children: React.ReactNode }) => (
+ handleSort(field)}
+ >
+ {children}
+
+
+ );
+
+ return (
+
+
+
+ Watchlist
+ {tokens.length}
+
+
+
+
+
+
+
+
+ Token
+
+
+ Price
+
+
+ 24h
+
+
+ Volume
+
+
+ MCap
+
+
+ Safety
+
+
+
+
+
+ {sortedTokens.map((token) => (
+ onTokenClick?.(token)}
+ >
+
+
+
+
+
+ {token.symbol}
+ {token.hasAlerts && (
+
+ )}
+
+
+ {token.name}
+
+
+
+
+
+ {formatPrice(token.price)}
+
+
+ 0 && "text-green-500",
+ token.priceChange24h < 0 && "text-red-500"
+ )}>
+ {formatPercent(token.priceChange24h)}
+
+
+
+ {formatLargeNumber(token.volume24h)}
+
+
+ {formatLargeNumber(token.marketCap)}
+
+
+
+
+
+
+ e.stopPropagation()}>
+
+
+
+
+
+ {
+ e.stopPropagation();
+ onConfigureAlerts?.(token);
+ }}>
+
+ Configure Alerts
+
+ {
+ e.stopPropagation();
+ window.open(`https://dexscreener.com/${token.chain}/${token.contractAddress}`, "_blank");
+ }}>
+
+ View on DexScreener
+
+ {
+ e.stopPropagation();
+ onRemoveToken?.(token.id);
+ }}
+ >
+
+ Remove
+
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
+
diff --git a/surfsense_web/components/crypto/index.ts b/surfsense_web/components/crypto/index.ts
new file mode 100644
index 000000000..af72730de
--- /dev/null
+++ b/surfsense_web/components/crypto/index.ts
@@ -0,0 +1,12 @@
+export { PriceDisplay } from "./PriceDisplay";
+export { SafetyBadge } from "./SafetyBadge";
+export { ChainIcon } from "./ChainIcon";
+export { MarketOverview } from "./MarketOverview";
+export { WatchlistTable } from "./WatchlistTable";
+export { AlertsPanel } from "./AlertsPanel";
+export { PortfolioSummary } from "./PortfolioSummary";
+
+// Modal Components
+export { AddTokenModal } from "./AddTokenModal";
+export { CreateAlertModal, type AlertConfig } from "./CreateAlertModal";
+export { UserProfileSection, type UserProfile } from "./UserProfileSection";
diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
index 537ae09ea..04457483b 100644
--- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
+++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
@@ -2,7 +2,7 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useAtomValue, useSetAtom } from "jotai";
-import { Inbox, LogOut, SquareLibrary, Trash2 } from "lucide-react";
+import { Coins, Inbox, LogOut, SquareLibrary, Trash2 } from "lucide-react";
import { useParams, usePathname, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useTheme } from "next-themes";
@@ -241,6 +241,12 @@ export function LayoutDataProvider({
icon: SquareLibrary,
isActive: pathname?.includes("/documents"),
},
+ {
+ title: "Crypto",
+ url: `/dashboard/${searchSpaceId}/crypto`,
+ icon: Coins,
+ isActive: pathname?.includes("/crypto"),
+ },
],
[searchSpaceId, pathname, isInboxSidebarOpen, totalUnreadCount]
);
diff --git a/surfsense_web/components/tool-ui/crypto/action-confirmation.tsx b/surfsense_web/components/tool-ui/crypto/action-confirmation.tsx
new file mode 100644
index 000000000..097c9ddd2
--- /dev/null
+++ b/surfsense_web/components/tool-ui/crypto/action-confirmation.tsx
@@ -0,0 +1,130 @@
+"use client";
+
+import { makeAssistantToolUI } from "@assistant-ui/react";
+import { z } from "zod";
+import { cn } from "@/lib/utils";
+import { CheckCircle, Star, Bell, Trash2, Eye, Settings } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+
+// Schema for action confirmation tool arguments
+export const ActionConfirmationArgsSchema = z.object({
+ actionType: z.enum(["watchlist_add", "watchlist_remove", "alert_set", "alert_delete"]),
+ tokenSymbol: z.string(),
+ details: z.array(z.string()).optional(),
+});
+
+export type ActionConfirmationArgs = z.infer;
+
+// Schema for action confirmation result
+export const ActionConfirmationResultSchema = z.object({
+ success: z.boolean(),
+ message: z.string().optional(),
+});
+
+export type ActionConfirmationResult = z.infer;
+
+const ACTION_CONFIG = {
+ watchlist_add: {
+ icon: Star,
+ title: "Added to Watchlist",
+ iconColor: "text-yellow-500",
+ bgColor: "bg-yellow-500/10",
+ },
+ watchlist_remove: {
+ icon: Trash2,
+ title: "Removed from Watchlist",
+ iconColor: "text-red-500",
+ bgColor: "bg-red-500/10",
+ },
+ alert_set: {
+ icon: Bell,
+ title: "Alert Created",
+ iconColor: "text-blue-500",
+ bgColor: "bg-blue-500/10",
+ },
+ alert_delete: {
+ icon: Trash2,
+ title: "Alert Deleted",
+ iconColor: "text-red-500",
+ bgColor: "bg-red-500/10",
+ },
+};
+
+/**
+ * ActionConfirmationToolUI - Shows confirmation when AI executes actions
+ * Used for watchlist add/remove, alert set/delete confirmations
+ */
+export const ActionConfirmationToolUI = makeAssistantToolUI({
+ toolName: "confirm_action",
+ render: ({ args, result, status }) => {
+ const isLoading = status.type === "running";
+ const config = ACTION_CONFIG[args.actionType];
+ const Icon = config.icon;
+
+ return (
+
+
+
+ {/* Icon */}
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Content */}
+
+
+
+ {config.title}
+
+ {args.tokenSymbol}
+
+
+
+ {/* Details */}
+ {args.details && args.details.length > 0 && (
+
+
Default monitoring enabled:
+
+ {args.details.map((detail, i) => (
+ {detail}
+ ))}
+
+
+ )}
+
+ {/* Result message */}
+ {result?.message && (
+
{result.message}
+ )}
+
+
+
+ {/* Action buttons */}
+
+
+
+ View Watchlist
+
+ {(args.actionType === "watchlist_add" || args.actionType === "alert_set") && (
+
+
+ Edit Alerts
+
+ )}
+
+
+
+ );
+ },
+});
+
diff --git a/surfsense_web/components/tool-ui/crypto/alert-configuration.tsx b/surfsense_web/components/tool-ui/crypto/alert-configuration.tsx
new file mode 100644
index 000000000..453c4e378
--- /dev/null
+++ b/surfsense_web/components/tool-ui/crypto/alert-configuration.tsx
@@ -0,0 +1,121 @@
+"use client";
+
+import { makeAssistantToolUI } from "@assistant-ui/react";
+import { z } from "zod";
+import { cn } from "@/lib/utils";
+import { Bell, TrendingUp, TrendingDown, Percent, DollarSign, Activity, Trash2, Edit2 } 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 { Switch } from "@/components/ui/switch";
+
+// Schema for alert configuration
+const AlertConfigSchema = z.object({
+ id: z.string(),
+ type: z.enum(["price_above", "price_below", "percent_change", "volume_spike", "whale_activity"]),
+ value: z.number(),
+ enabled: z.boolean(),
+});
+
+// Schema for alert configuration tool arguments
+export const AlertConfigurationArgsSchema = z.object({
+ tokenSymbol: z.string(),
+ tokenName: z.string().optional(),
+ alerts: z.array(AlertConfigSchema),
+});
+
+export type AlertConfigurationArgs = z.infer;
+
+// Schema for alert configuration result
+export const AlertConfigurationResultSchema = z.object({
+ success: z.boolean(),
+ message: z.string().optional(),
+});
+
+export type AlertConfigurationResult = z.infer;
+
+const ALERT_TYPE_CONFIG = {
+ price_above: { icon: TrendingUp, label: "Price Above", color: "text-green-500" },
+ price_below: { icon: TrendingDown, label: "Price Below", color: "text-red-500" },
+ percent_change: { icon: Percent, label: "% Change", color: "text-blue-500" },
+ volume_spike: { icon: Activity, label: "Volume Spike", color: "text-purple-500" },
+ whale_activity: { icon: DollarSign, label: "Whale Activity", color: "text-orange-500" },
+};
+
+const formatValue = (type: string, value: number): string => {
+ if (type === "percent_change") return `${value > 0 ? "+" : ""}${value}%`;
+ if (type === "volume_spike") return `${value}x normal`;
+ if (type === "whale_activity") return `>${value.toLocaleString()} USD`;
+ return `$${value < 1 ? value.toFixed(6) : value.toLocaleString()}`;
+};
+
+/**
+ * AlertConfigurationToolUI - Displays/edits alert configurations for a token
+ * Used when AI responds to "set alert for BULLA" or "show my alerts for BULLA"
+ */
+export const AlertConfigurationToolUI = makeAssistantToolUI({
+ toolName: "configure_alerts",
+ render: ({ args, status }) => {
+ const isLoading = status.type === "running";
+ const alerts = args.alerts || [];
+ const enabledCount = alerts.filter(a => a.enabled).length;
+
+ return (
+
+
+
+
+
+ Alerts for {args.tokenSymbol}
+ {enabledCount} active
+ {isLoading && Loading... }
+
+
+
+ Add Alert
+
+
+
+
+ {alerts.length === 0 ? (
+
+
+
No alerts configured
+
Say "Alert me if {args.tokenSymbol} drops 20%"
+
+ ) : (
+
+ {alerts.map((alert) => {
+ const config = ALERT_TYPE_CONFIG[alert.type];
+ const Icon = config.icon;
+ return (
+
+
+
+
+
{config.label}
+
+ {formatValue(alert.type, alert.value)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+
+ );
+ },
+});
+
diff --git a/surfsense_web/components/tool-ui/crypto/holder-analysis.tsx b/surfsense_web/components/tool-ui/crypto/holder-analysis.tsx
new file mode 100644
index 000000000..878ce73a4
--- /dev/null
+++ b/surfsense_web/components/tool-ui/crypto/holder-analysis.tsx
@@ -0,0 +1,144 @@
+"use client";
+
+import { makeAssistantToolUI } from "@assistant-ui/react";
+import { z } from "zod";
+import { cn } from "@/lib/utils";
+import { Users, AlertTriangle, Shield, Crown } from "lucide-react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { ChainIcon } from "@/components/crypto/ChainIcon";
+
+// Schema for holder
+const HolderSchema = z.object({
+ rank: z.number(),
+ address: z.string(),
+ label: z.string().optional(),
+ balance: z.number(),
+ percentage: z.number(),
+ isContract: z.boolean().optional(),
+});
+
+// Schema for holder analysis tool arguments
+export const HolderAnalysisArgsSchema = z.object({
+ tokenSymbol: z.string(),
+ chain: z.string(),
+ totalHolders: z.number(),
+ top10Percentage: z.number(),
+ top50Percentage: z.number().optional(),
+ holders: z.array(HolderSchema),
+ concentrationRisk: z.enum(["low", "medium", "high", "critical"]).optional(),
+});
+
+export type HolderAnalysisArgs = z.infer;
+
+// Schema for holder analysis result
+export const HolderAnalysisResultSchema = z.object({
+ success: z.boolean(),
+ message: z.string().optional(),
+});
+
+export type HolderAnalysisResult = z.infer;
+
+const shortenAddress = (address: string): string => {
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
+};
+
+const formatBalance = (balance: number): string => {
+ if (balance >= 1e9) return `${(balance / 1e9).toFixed(2)}B`;
+ if (balance >= 1e6) return `${(balance / 1e6).toFixed(2)}M`;
+ if (balance >= 1e3) return `${(balance / 1e3).toFixed(2)}K`;
+ return balance.toFixed(2);
+};
+
+const getRiskColor = (risk: string) => {
+ switch (risk) {
+ case "low": return "text-green-500 bg-green-500/10";
+ case "medium": return "text-yellow-500 bg-yellow-500/10";
+ case "high": return "text-orange-500 bg-orange-500/10";
+ case "critical": return "text-red-500 bg-red-500/10";
+ default: return "text-muted-foreground bg-muted";
+ }
+};
+
+/**
+ * HolderAnalysisToolUI - Displays holder distribution inline in chat
+ * Used when AI responds to "who holds BULLA?" or "analyze holders"
+ */
+export const HolderAnalysisToolUI = makeAssistantToolUI({
+ toolName: "analyze_holders",
+ render: ({ args, status }) => {
+ const isLoading = status.type === "running";
+ const holders = args.holders || [];
+ const risk = args.concentrationRisk || "medium";
+
+ return (
+
+
+
+
+
+ Holder Analysis - {args.tokenSymbol}
+ {isLoading && Loading... }
+
+
+
+
+
+ {/* Summary Stats */}
+
+
+
Total Holders
+
{args.totalHolders.toLocaleString()}
+
+
50 ? "bg-red-500/10" : "bg-muted/50")}>
+
Top 10 Hold
+
50 && "text-red-500")}>{args.top10Percentage.toFixed(1)}%
+
+ {args.top50Percentage && (
+
+
Top 50 Hold
+
{args.top50Percentage.toFixed(1)}%
+
+ )}
+
+
Concentration Risk
+
{risk}
+
+
+
+ {/* Risk Warning */}
+ {(risk === "high" || risk === "critical") && (
+
+
+
High holder concentration detected. Top wallets could significantly impact price.
+
+ )}
+
+ {/* Top Holders List */}
+
+
Top Holders
+
+ {holders.slice(0, 10).map((holder) => (
+
+
+
#{holder.rank}
+ {holder.rank <= 3 &&
}
+
+
{holder.label || shortenAddress(holder.address)}
+ {holder.isContract &&
Contract }
+
+
+
+
{holder.percentage.toFixed(2)}%
+
{formatBalance(holder.balance)}
+
+
+ ))}
+
+
+
+
+ );
+ },
+});
+
diff --git a/surfsense_web/components/tool-ui/crypto/market-overview-tool.tsx b/surfsense_web/components/tool-ui/crypto/market-overview-tool.tsx
new file mode 100644
index 000000000..e2397c4df
--- /dev/null
+++ b/surfsense_web/components/tool-ui/crypto/market-overview-tool.tsx
@@ -0,0 +1,126 @@
+"use client";
+
+import { makeAssistantToolUI } from "@assistant-ui/react";
+import { z } from "zod";
+import { cn } from "@/lib/utils";
+import { BarChart3, TrendingUp, TrendingDown, Globe } from "lucide-react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+
+// Schema for market token
+const MarketTokenSchema = z.object({
+ symbol: z.string(),
+ name: z.string(),
+ price: z.number(),
+ priceChange24h: z.number(),
+ marketCap: z.number().optional(),
+ volume24h: z.number().optional(),
+});
+
+// Schema for market overview tool arguments
+export const MarketOverviewArgsSchema = z.object({
+ tokens: z.array(MarketTokenSchema),
+ totalMarketCap: z.number().optional(),
+ totalVolume24h: z.number().optional(),
+ btcDominance: z.number().optional(),
+ fearGreedIndex: z.number().optional(),
+});
+
+export type MarketOverviewArgs = z.infer;
+
+// Schema for market overview result
+export const MarketOverviewResultSchema = z.object({
+ success: z.boolean(),
+ message: z.string().optional(),
+});
+
+export type MarketOverviewResult = z.infer;
+
+const formatPrice = (price: number): string => {
+ if (price < 1) return `$${price.toFixed(4)}`;
+ return `$${price.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
+};
+
+const formatLargeNumber = (num: number): string => {
+ if (num >= 1e12) return `$${(num / 1e12).toFixed(2)}T`;
+ if (num >= 1e9) return `$${(num / 1e9).toFixed(2)}B`;
+ if (num >= 1e6) return `$${(num / 1e6).toFixed(2)}M`;
+ return `$${num.toFixed(2)}`;
+};
+
+/**
+ * MarketOverviewToolUI - Displays market overview inline in chat
+ * Used when AI responds to "show market overview" or "how's the market?"
+ */
+export const MarketOverviewToolUI = makeAssistantToolUI({
+ toolName: "get_market_overview",
+ render: ({ args, status }) => {
+ const isLoading = status.type === "running";
+ const tokens = args.tokens || [];
+
+ return (
+
+
+
+
+ Market Overview
+ {isLoading && Loading... }
+
+
+
+ {/* Global Stats */}
+ {(args.totalMarketCap || args.btcDominance || args.fearGreedIndex) && (
+
+ {args.totalMarketCap && (
+
+
Total Market Cap
+
{formatLargeNumber(args.totalMarketCap)}
+
+ )}
+ {args.totalVolume24h && (
+
+
24h Volume
+
{formatLargeNumber(args.totalVolume24h)}
+
+ )}
+ {args.btcDominance && (
+
+
BTC Dominance
+
{args.btcDominance.toFixed(1)}%
+
+ )}
+ {args.fearGreedIndex && (
+
50 ? "bg-green-500/10" : "bg-red-500/10")}>
+
Fear & Greed
+
50 ? "text-green-500" : "text-red-500")}>
+ {args.fearGreedIndex} - {args.fearGreedIndex > 75 ? "Extreme Greed" : args.fearGreedIndex > 50 ? "Greed" : args.fearGreedIndex > 25 ? "Fear" : "Extreme Fear"}
+
+
+ )}
+
+ )}
+
+ {/* Token Prices */}
+
+ {tokens.map((token) => (
+
+
+
{token.symbol}
+
{token.name}
+
+
+
{formatPrice(token.price)}
+
= 0 ? "text-green-500" : "text-red-500")}>
+ {token.priceChange24h >= 0 ? : }
+ {token.priceChange24h >= 0 ? "+" : ""}{token.priceChange24h.toFixed(2)}%
+
+
+
+ ))}
+
+
+
+ );
+ },
+});
+
diff --git a/surfsense_web/components/tool-ui/crypto/portfolio-display.tsx b/surfsense_web/components/tool-ui/crypto/portfolio-display.tsx
new file mode 100644
index 000000000..a72eb90c3
--- /dev/null
+++ b/surfsense_web/components/tool-ui/crypto/portfolio-display.tsx
@@ -0,0 +1,121 @@
+"use client";
+
+import { makeAssistantToolUI } from "@assistant-ui/react";
+import { z } from "zod";
+import { cn } from "@/lib/utils";
+import { Wallet, TrendingUp, TrendingDown, PieChart } from "lucide-react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { ChainIcon } from "@/components/crypto/ChainIcon";
+
+// Schema for portfolio holding
+const HoldingSchema = z.object({
+ symbol: z.string(),
+ name: z.string(),
+ chain: z.string(),
+ balance: z.number(),
+ value: z.number(),
+ costBasis: z.number().optional(),
+ pnl: z.number().optional(),
+ pnlPercent: z.number().optional(),
+ allocation: z.number(),
+});
+
+// Schema for portfolio display tool arguments
+export const PortfolioDisplayArgsSchema = z.object({
+ holdings: z.array(HoldingSchema),
+ totalValue: z.number(),
+ totalPnl: z.number().optional(),
+ totalPnlPercent: z.number().optional(),
+ lastUpdated: z.string().optional(),
+});
+
+export type PortfolioDisplayArgs = z.infer;
+
+// Schema for portfolio display result
+export const PortfolioDisplayResultSchema = z.object({
+ success: z.boolean(),
+ message: z.string().optional(),
+});
+
+export type PortfolioDisplayResult = z.infer;
+
+const formatValue = (value: number): string => {
+ if (value >= 1e6) return `$${(value / 1e6).toFixed(2)}M`;
+ if (value >= 1e3) return `$${(value / 1e3).toFixed(2)}K`;
+ return `$${value.toFixed(2)}`;
+};
+
+/**
+ * PortfolioDisplayToolUI - Displays user's portfolio inline in chat
+ * Used when AI responds to "how's my portfolio?" or "show my holdings"
+ */
+export const PortfolioDisplayToolUI = makeAssistantToolUI({
+ toolName: "get_portfolio",
+ render: ({ args, status }) => {
+ const isLoading = status.type === "running";
+ const holdings = args.holdings || [];
+ const hasPnl = args.totalPnl !== undefined;
+
+ return (
+
+
+
+
+
+ Your Portfolio
+ {isLoading && Loading... }
+
+ {args.lastUpdated && (
+ Updated {args.lastUpdated}
+ )}
+
+
+
+ {/* Total Value */}
+
+
Total Value
+
{formatValue(args.totalValue)}
+ {hasPnl && (
+
= 0 ? "text-green-500" : "text-red-500")}>
+ {(args.totalPnl || 0) >= 0 ? : }
+ {(args.totalPnl || 0) >= 0 ? "+" : ""}{formatValue(args.totalPnl || 0)} ({(args.totalPnlPercent || 0) >= 0 ? "+" : ""}{(args.totalPnlPercent || 0).toFixed(2)}%)
+
+ )}
+
+
+ {/* Holdings List */}
+ {holdings.length === 0 ? (
+ No holdings found
+ ) : (
+
+ {holdings.map((holding) => (
+
+
+
+
+
+ {holding.symbol}
+ {holding.allocation.toFixed(1)}%
+
+
{holding.balance.toLocaleString()} tokens
+
+
+
+
{formatValue(holding.value)}
+ {holding.pnlPercent !== undefined && (
+
= 0 ? "text-green-500" : "text-red-500")}>
+ {holding.pnlPercent >= 0 ? "+" : ""}{holding.pnlPercent.toFixed(2)}%
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+ );
+ },
+});
+
diff --git a/surfsense_web/components/tool-ui/crypto/proactive-alert.tsx b/surfsense_web/components/tool-ui/crypto/proactive-alert.tsx
new file mode 100644
index 000000000..91fb206d3
--- /dev/null
+++ b/surfsense_web/components/tool-ui/crypto/proactive-alert.tsx
@@ -0,0 +1,136 @@
+"use client";
+
+import { makeAssistantToolUI } from "@assistant-ui/react";
+import { z } from "zod";
+import { cn } from "@/lib/utils";
+import { AlertTriangle, TrendingUp, TrendingDown, Activity, Zap, Eye, Bell, X } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+
+// Schema for proactive alert tool arguments
+export const ProactiveAlertArgsSchema = z.object({
+ alertType: z.enum(["price_surge", "price_drop", "whale_buy", "whale_sell", "volume_spike", "safety_warning"]),
+ tokenSymbol: z.string(),
+ tokenName: z.string().optional(),
+ value: z.number(),
+ previousValue: z.number().optional(),
+ message: z.string(),
+ severity: z.enum(["info", "warning", "critical"]).optional(),
+ timestamp: z.string().optional(),
+});
+
+export type ProactiveAlertArgs = z.infer;
+
+// Schema for proactive alert result
+export const ProactiveAlertResultSchema = z.object({
+ acknowledged: z.boolean(),
+});
+
+export type ProactiveAlertResult = z.infer;
+
+const ALERT_TYPE_CONFIG = {
+ price_surge: { icon: TrendingUp, color: "text-green-500", bgColor: "bg-green-500/10", borderColor: "border-l-green-500" },
+ price_drop: { icon: TrendingDown, color: "text-red-500", bgColor: "bg-red-500/10", borderColor: "border-l-red-500" },
+ whale_buy: { icon: Zap, color: "text-green-500", bgColor: "bg-green-500/10", borderColor: "border-l-green-500" },
+ whale_sell: { icon: Zap, color: "text-red-500", bgColor: "bg-red-500/10", borderColor: "border-l-red-500" },
+ volume_spike: { icon: Activity, color: "text-purple-500", bgColor: "bg-purple-500/10", borderColor: "border-l-purple-500" },
+ safety_warning: { icon: AlertTriangle, color: "text-yellow-500", bgColor: "bg-yellow-500/10", borderColor: "border-l-yellow-500" },
+};
+
+const SEVERITY_CONFIG = {
+ info: { badge: "secondary", pulse: false },
+ warning: { badge: "warning", pulse: false },
+ critical: { badge: "destructive", pulse: true },
+};
+
+/**
+ * ProactiveAlertToolUI - Displays AI-initiated alerts in chat
+ * Used when AI proactively notifies user about price changes, whale activity, etc.
+ */
+export const ProactiveAlertToolUI = makeAssistantToolUI({
+ toolName: "proactive_alert",
+ render: ({ args, result }) => {
+ const config = ALERT_TYPE_CONFIG[args.alertType];
+ const severity = args.severity || "info";
+ const severityConfig = SEVERITY_CONFIG[severity];
+ const Icon = config.icon;
+ const isAcknowledged = result?.acknowledged;
+
+ const formatChange = () => {
+ if (args.previousValue === undefined) return null;
+ const change = ((args.value - args.previousValue) / args.previousValue) * 100;
+ return change;
+ };
+
+ const change = formatChange();
+
+ return (
+
+
+
+ {/* Alert Icon */}
+
+
+
+
+ {/* Content */}
+
+
+
+ {args.alertType.replace("_", " ")}
+
+ {args.tokenSymbol}
+ {change !== null && (
+ = 0 ? "text-green-500" : "text-red-500"
+ )}>
+ {change >= 0 ? "+" : ""}{change.toFixed(1)}%
+
+ )}
+ {args.timestamp && (
+
+ {args.timestamp}
+
+ )}
+
+
+
{args.message}
+
+
+ {/* Dismiss */}
+ {!isAcknowledged && (
+
+
+
+ )}
+
+
+ {/* Action buttons */}
+ {!isAcknowledged && (
+
+
+
+ View Details
+
+
+
+ Adjust Alert
+
+
+ )}
+
+
+ );
+ },
+});
+
diff --git a/surfsense_web/components/tool-ui/crypto/token-analysis.tsx b/surfsense_web/components/tool-ui/crypto/token-analysis.tsx
new file mode 100644
index 000000000..cfde4bb00
--- /dev/null
+++ b/surfsense_web/components/tool-ui/crypto/token-analysis.tsx
@@ -0,0 +1,162 @@
+"use client";
+
+import { makeAssistantToolUI } from "@assistant-ui/react";
+import { z } from "zod";
+import { cn } from "@/lib/utils";
+import { Shield, TrendingUp, TrendingDown, Users, AlertTriangle, Star, Bell, ExternalLink } 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";
+import { SafetyBadge } from "@/components/crypto/SafetyBadge";
+
+// Schema for token analysis tool arguments
+export const TokenAnalysisArgsSchema = z.object({
+ symbol: z.string(),
+ name: z.string().optional(),
+ chain: z.string(),
+ contractAddress: z.string().optional(),
+ price: z.number(),
+ priceChange24h: z.number(),
+ marketCap: z.number().optional(),
+ volume24h: z.number().optional(),
+ liquidity: z.number().optional(),
+ safetyScore: z.number().optional(),
+ holderCount: z.number().optional(),
+ top10HolderPercent: z.number().optional(),
+});
+
+export type TokenAnalysisArgs = z.infer;
+
+// Schema for token analysis result
+export const TokenAnalysisResultSchema = z.object({
+ success: z.boolean(),
+ message: z.string().optional(),
+ isInWatchlist: z.boolean().optional(),
+});
+
+export type TokenAnalysisResult = z.infer;
+
+const formatPrice = (price: number): string => {
+ if (price < 0.00001) return `$${price.toExponential(2)}`;
+ if (price < 1) return `$${price.toFixed(6)}`;
+ return `$${price.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
+};
+
+const formatLargeNumber = (num: number): string => {
+ 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)}`;
+};
+
+/**
+ * TokenAnalysisToolUI - Displays comprehensive token analysis in chat
+ * Used when AI responds to token research queries like "analyze BULLA" or "is BULLA safe?"
+ */
+export const TokenAnalysisToolUI = makeAssistantToolUI({
+ toolName: "analyze_token",
+ render: ({ args, result, status }) => {
+ const isLoading = status.type === "running";
+ const isInWatchlist = result?.isInWatchlist ?? false;
+
+ const handleOpenDexScreener = () => {
+ if (args.contractAddress) {
+ window.open(`https://dexscreener.com/${args.chain}/${args.contractAddress}`, "_blank");
+ }
+ };
+
+ return (
+
+
+
+ π
+ Token Analysis
+ {isLoading && Analyzing... }
+
+
+
+ {/* Token Header */}
+
+
+
+
+
+ {args.symbol}
+ {args.name && {args.name} }
+
+
+ {formatPrice(args.price)}
+ = 0 ? "text-green-500" : "text-red-500"
+ )}>
+ {args.priceChange24h >= 0 ? : }
+ {args.priceChange24h >= 0 ? "+" : ""}{args.priceChange24h.toFixed(2)}%
+
+
+
+
+ {args.safetyScore !== undefined && (
+
+ )}
+
+
+ {/* Metrics Grid */}
+
+ {args.marketCap && (
+
+
Market Cap
+
{formatLargeNumber(args.marketCap)}
+
+ )}
+ {args.volume24h && (
+
+
24h Volume
+
{formatLargeNumber(args.volume24h)}
+
+ )}
+ {args.liquidity && (
+
+
Liquidity
+
{formatLargeNumber(args.liquidity)}
+
+ )}
+ {args.holderCount && (
+
+
Holders
+
+
+ {args.holderCount.toLocaleString()}
+
+
+ )}
+
+
+ {/* Holder Concentration Warning */}
+ {args.top10HolderPercent && args.top10HolderPercent > 50 && (
+
+
+
Top 10 holders own {args.top10HolderPercent}% of supply - high concentration risk
+
+ )}
+
+ {/* Action Buttons */}
+
+
+
+ {isInWatchlist ? "In Watchlist" : "Add to Watchlist"}
+
+
+
+
+
+
+
+
+
+
+ );
+ },
+});
+
diff --git a/surfsense_web/components/tool-ui/crypto/trending-tokens.tsx b/surfsense_web/components/tool-ui/crypto/trending-tokens.tsx
new file mode 100644
index 000000000..9db05ded0
--- /dev/null
+++ b/surfsense_web/components/tool-ui/crypto/trending-tokens.tsx
@@ -0,0 +1,124 @@
+"use client";
+
+import { makeAssistantToolUI } from "@assistant-ui/react";
+import { z } from "zod";
+import { cn } from "@/lib/utils";
+import { Flame, TrendingUp, TrendingDown, Star, ExternalLink } 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 trending token
+const TrendingTokenSchema = z.object({
+ symbol: z.string(),
+ name: z.string(),
+ chain: z.string(),
+ contractAddress: z.string().optional(),
+ price: z.number(),
+ priceChange24h: z.number(),
+ priceChange1h: z.number().optional(),
+ volume24h: z.number().optional(),
+ liquidity: z.number().optional(),
+ rank: z.number().optional(),
+});
+
+// Schema for trending tokens tool arguments
+export const TrendingTokensArgsSchema = z.object({
+ chain: z.string().optional(),
+ tokens: z.array(TrendingTokenSchema),
+ timeframe: z.string().optional(),
+});
+
+export type TrendingTokensArgs = z.infer;
+
+// Schema for trending tokens result
+export const TrendingTokensResultSchema = z.object({
+ success: z.boolean(),
+ message: z.string().optional(),
+});
+
+export type TrendingTokensResult = z.infer;
+
+const formatPrice = (price: number): string => {
+ if (price < 0.00001) return `$${price.toExponential(2)}`;
+ if (price < 1) return `$${price.toFixed(6)}`;
+ return `$${price.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
+};
+
+const formatLargeNumber = (num: number): string => {
+ 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)}`;
+};
+
+/**
+ * TrendingTokensToolUI - Displays trending/hot tokens inline in chat
+ * Used when AI responds to "what's hot on Solana?" or "show trending tokens"
+ */
+export const TrendingTokensToolUI = makeAssistantToolUI({
+ toolName: "get_trending_tokens",
+ render: ({ args, status }) => {
+ const isLoading = status.type === "running";
+ const tokens = args.tokens || [];
+ const chain = args.chain || "all chains";
+ const timeframe = args.timeframe || "24h";
+
+ return (
+
+
+
+
+
+ Trending on {chain}
+ {timeframe}
+ {isLoading && Loading... }
+
+
+
+
+ {tokens.length === 0 ? (
+ No trending tokens found
+ ) : (
+
+ {tokens.map((token, index) => (
+
+
+
#{token.rank || index + 1}
+
+
+
+ {token.symbol}
+
+
{token.name}
+
+
+
+
+
{formatPrice(token.price)}
+
= 0 ? "text-green-500" : "text-red-500")}>
+ {token.priceChange24h >= 0 ? : }
+ {token.priceChange24h >= 0 ? "+" : ""}{token.priceChange24h.toFixed(1)}%
+
+
+ {token.volume24h && (
+
+
Volume
+
{formatLargeNumber(token.volume24h)}
+
+ )}
+
+
+
+
+
+ ))}
+
+ )}
+
+
+ );
+ },
+});
+
diff --git a/surfsense_web/components/tool-ui/crypto/user-profile.tsx b/surfsense_web/components/tool-ui/crypto/user-profile.tsx
new file mode 100644
index 000000000..78b1b07dd
--- /dev/null
+++ b/surfsense_web/components/tool-ui/crypto/user-profile.tsx
@@ -0,0 +1,142 @@
+"use client";
+
+import { makeAssistantToolUI } from "@assistant-ui/react";
+import { z } from "zod";
+import { cn } from "@/lib/utils";
+import { User, Shield, Target, Clock, Zap } from "lucide-react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+
+// Schema for user profile tool arguments
+export const UserProfileArgsSchema = z.object({
+ riskTolerance: z.enum(["conservative", "moderate", "aggressive"]),
+ investmentStyle: z.enum(["day_trader", "swing", "long_term"]),
+ preferredChains: z.array(z.string()),
+ portfolioSizeRange: z.enum(["small", "medium", "large"]).optional(),
+ experienceLevel: z.enum(["beginner", "intermediate", "advanced"]).optional(),
+ notificationPreferences: z.object({
+ priceAlerts: z.boolean(),
+ whaleAlerts: z.boolean(),
+ newsAlerts: z.boolean(),
+ }).optional(),
+});
+
+export type UserProfileArgs = z.infer;
+
+// Schema for user profile result
+export const UserProfileResultSchema = z.object({
+ success: z.boolean(),
+ message: z.string().optional(),
+});
+
+export type UserProfileResult = z.infer;
+
+const getRiskColor = (risk: string) => {
+ switch (risk) {
+ case "conservative": return "text-green-500 bg-green-500/10 border-green-500/20";
+ case "moderate": return "text-yellow-500 bg-yellow-500/10 border-yellow-500/20";
+ case "aggressive": return "text-red-500 bg-red-500/10 border-red-500/20";
+ default: return "";
+ }
+};
+
+const getStyleIcon = (style: string) => {
+ switch (style) {
+ case "day_trader": return ;
+ case "swing": return ;
+ case "long_term": return ;
+ default: return null;
+ }
+};
+
+const formatStyle = (style: string) => {
+ switch (style) {
+ case "day_trader": return "Day Trader";
+ case "swing": return "Swing Trader";
+ case "long_term": return "Long Term Investor";
+ default: return style;
+ }
+};
+
+/**
+ * UserProfileToolUI - Displays user's investment profile inline in chat
+ * Used when AI responds to "show my profile" or "what's my risk setting?"
+ */
+export const UserProfileToolUI = makeAssistantToolUI({
+ toolName: "get_user_profile",
+ render: ({ args, status }) => {
+ const isLoading = status.type === "running";
+
+ return (
+
+
+
+
+ Your Investment Profile
+ {isLoading && Loading... }
+
+
+
+ {/* Main Profile Settings */}
+
+ {/* Risk Tolerance */}
+
+
+
+ Risk Tolerance
+
+
{args.riskTolerance}
+
+ {args.riskTolerance === "conservative" && "Prefer stable, lower-risk investments"}
+ {args.riskTolerance === "moderate" && "Balance between risk and reward"}
+ {args.riskTolerance === "aggressive" && "Willing to take higher risks for higher returns"}
+
+
+
+ {/* Investment Style */}
+
+
+ {getStyleIcon(args.investmentStyle)}
+ Investment Style
+
+
{formatStyle(args.investmentStyle)}
+
+ {args.investmentStyle === "day_trader" && "Quick trades, high frequency"}
+ {args.investmentStyle === "swing" && "Hold for days to weeks"}
+ {args.investmentStyle === "long_term" && "Hold for months to years"}
+
+
+
+
+ {/* Preferred Chains */}
+
+
Preferred Chains
+
+ {args.preferredChains.map((chain) => (
+ {chain}
+ ))}
+
+
+
+ {/* Notification Preferences */}
+ {args.notificationPreferences && (
+
+
Notifications
+
+ {args.notificationPreferences.priceAlerts && Price Alerts }
+ {args.notificationPreferences.whaleAlerts && Whale Alerts }
+ {args.notificationPreferences.newsAlerts && News Alerts }
+
+
+ )}
+
+ {/* Edit Hint */}
+
+ Say "update my risk tolerance to moderate" to change settings
+
+
+
+ );
+ },
+});
+
diff --git a/surfsense_web/components/tool-ui/crypto/watchlist-display.tsx b/surfsense_web/components/tool-ui/crypto/watchlist-display.tsx
new file mode 100644
index 000000000..d6e783b0e
--- /dev/null
+++ b/surfsense_web/components/tool-ui/crypto/watchlist-display.tsx
@@ -0,0 +1,145 @@
+"use client";
+
+import { makeAssistantToolUI } from "@assistant-ui/react";
+import { z } from "zod";
+import { cn } from "@/lib/utils";
+import { Star, TrendingUp, TrendingDown, Bell, Trash2, Plus } 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 watchlist token
+const WatchlistTokenSchema = z.object({
+ id: z.string(),
+ symbol: z.string(),
+ name: z.string(),
+ chain: z.string(),
+ price: z.number(),
+ priceChange24h: z.number(),
+ alertCount: z.number().optional(),
+});
+
+// Schema for watchlist display tool arguments
+export const WatchlistDisplayArgsSchema = z.object({
+ tokens: z.array(WatchlistTokenSchema),
+});
+
+export type WatchlistDisplayArgs = z.infer;
+
+// Schema for watchlist display result
+export const WatchlistDisplayResultSchema = z.object({
+ success: z.boolean(),
+ message: z.string().optional(),
+});
+
+export type WatchlistDisplayResult = z.infer;
+
+const formatPrice = (price: number): string => {
+ if (price < 0.00001) return `$${price.toExponential(2)}`;
+ if (price < 1) return `$${price.toFixed(6)}`;
+ return `$${price.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
+};
+
+/**
+ * WatchlistDisplayToolUI - Displays user's watchlist inline in chat
+ * Used when AI responds to "show my watchlist" or similar commands
+ */
+export const WatchlistDisplayToolUI = makeAssistantToolUI({
+ toolName: "show_watchlist",
+ render: ({ args, status }) => {
+ const isLoading = status.type === "running";
+ const tokens = args.tokens || [];
+
+ if (tokens.length === 0) {
+ return (
+
+
+
+ Your watchlist is empty
+
+ Say "Add [token] to my watchlist" to start tracking
+
+
+
+ );
+ }
+
+ // Find best and worst performers
+ const sortedByChange = [...tokens].sort((a, b) => b.priceChange24h - a.priceChange24h);
+ const bestPerformer = sortedByChange[0];
+ const worstPerformer = sortedByChange[sortedByChange.length - 1];
+
+ return (
+
+
+
+
+
+ Your Watchlist
+ {tokens.length}
+ {isLoading && Loading... }
+
+
+
+ Add Token
+
+
+
+
+ {/* Token List */}
+
+ {tokens.map((token) => (
+
+
+
+
+
+ {token.symbol}
+ {token.alertCount && token.alertCount > 0 && (
+
+
+ {token.alertCount}
+
+ )}
+
+
{token.name}
+
+
+
+
+
{formatPrice(token.price)}
+
= 0 ? "text-green-500" : "text-red-500"
+ )}>
+ {token.priceChange24h >= 0 ? : }
+ {token.priceChange24h >= 0 ? "+" : ""}{token.priceChange24h.toFixed(1)}%
+
+
+
+
+
+
+
+ ))}
+
+
+ {/* Summary */}
+ {tokens.length > 1 && (
+
+ {bestPerformer.symbol} is your best performer (+{bestPerformer.priceChange24h.toFixed(1)}%)
+ {worstPerformer.priceChange24h < 0 && (
+ β’ {worstPerformer.symbol} needs attention ({worstPerformer.priceChange24h.toFixed(1)}%)
+ )}
+
+ )}
+
+
+ );
+ },
+});
+
diff --git a/surfsense_web/components/tool-ui/crypto/whale-activity.tsx b/surfsense_web/components/tool-ui/crypto/whale-activity.tsx
new file mode 100644
index 000000000..8fb0ef60b
--- /dev/null
+++ b/surfsense_web/components/tool-ui/crypto/whale-activity.tsx
@@ -0,0 +1,154 @@
+"use client";
+
+import { makeAssistantToolUI } from "@assistant-ui/react";
+import { z } from "zod";
+import { cn } from "@/lib/utils";
+import { Fish, ArrowUpRight, ArrowDownRight, ExternalLink, Clock } 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 whale transaction
+const WhaleTransactionSchema = z.object({
+ id: z.string(),
+ type: z.enum(["buy", "sell", "transfer"]),
+ amount: z.number(),
+ amountUsd: z.number(),
+ tokenSymbol: z.string(),
+ walletAddress: z.string(),
+ walletLabel: z.string().optional(),
+ timestamp: z.string(),
+ txHash: z.string().optional(),
+});
+
+// Schema for whale activity tool arguments
+export const WhaleActivityArgsSchema = z.object({
+ tokenSymbol: z.string(),
+ chain: z.string(),
+ transactions: z.array(WhaleTransactionSchema),
+ summary: z.object({
+ totalBuyVolume: z.number(),
+ totalSellVolume: z.number(),
+ netFlow: z.number(),
+ uniqueWhales: z.number(),
+ }).optional(),
+});
+
+export type WhaleActivityArgs = z.infer;
+
+// Schema for whale activity result
+export const WhaleActivityResultSchema = z.object({
+ success: z.boolean(),
+ message: z.string().optional(),
+});
+
+export type WhaleActivityResult = z.infer;
+
+const formatLargeNumber = (num: number): string => {
+ if (num >= 1e6) return `$${(num / 1e6).toFixed(2)}M`;
+ if (num >= 1e3) return `$${(num / 1e3).toFixed(2)}K`;
+ return `$${num.toFixed(2)}`;
+};
+
+const shortenAddress = (address: string): string => {
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
+};
+
+const formatTimeAgo = (timestamp: string): string => {
+ const diff = Date.now() - new Date(timestamp).getTime();
+ const minutes = Math.floor(diff / 60000);
+ if (minutes < 60) return `${minutes}m ago`;
+ const hours = Math.floor(minutes / 60);
+ if (hours < 24) return `${hours}h ago`;
+ return `${Math.floor(hours / 24)}d ago`;
+};
+
+/**
+ * WhaleActivityToolUI - Displays whale transactions inline in chat
+ * Used when AI responds to "show whale activity for BULLA" or similar
+ */
+export const WhaleActivityToolUI = makeAssistantToolUI({
+ toolName: "get_whale_activity",
+ render: ({ args, status }) => {
+ const isLoading = status.type === "running";
+ const transactions = args.transactions || [];
+ const summary = args.summary;
+
+ return (
+
+
+
+
+
+ Whale Activity - {args.tokenSymbol}
+ {isLoading && Loading... }
+
+
+
+
+
+ {/* Summary Stats */}
+ {summary && (
+
+
+
Buy Volume
+
{formatLargeNumber(summary.totalBuyVolume)}
+
+
+
Sell Volume
+
{formatLargeNumber(summary.totalSellVolume)}
+
+
= 0 ? "bg-green-500/10" : "bg-red-500/10")}>
+
Net Flow
+
= 0 ? "text-green-500" : "text-red-500")}>
+ {summary.netFlow >= 0 ? "+" : ""}{formatLargeNumber(summary.netFlow)}
+
+
+
+
Unique Whales
+
{summary.uniqueWhales}
+
+
+ )}
+
+ {/* Transaction List */}
+ {transactions.length === 0 ? (
+ No whale transactions detected
+ ) : (
+
+ {transactions.map((tx) => (
+
+
+
+ {tx.type === "buy" ?
: tx.type === "sell" ?
:
}
+
+
+
+
+ {tx.type}
+
+ {formatLargeNumber(tx.amountUsd)}
+
+
+ {tx.walletLabel || shortenAddress(tx.walletAddress)}
+
+ {formatTimeAgo(tx.timestamp)}
+
+
+
+ {tx.txHash && (
+
+
+
+ )}
+
+ ))}
+
+ )}
+
+
+ );
+ },
+});
+
diff --git a/surfsense_web/components/tool-ui/index.ts b/surfsense_web/components/tool-ui/index.ts
index 5b4ea0a34..a1edea3cf 100644
--- a/surfsense_web/components/tool-ui/index.ts
+++ b/surfsense_web/components/tool-ui/index.ts
@@ -91,3 +91,37 @@ export {
SaveMemoryToolUI,
} from "./user-memory";
export { type WriteTodosData, WriteTodosSchema, WriteTodosToolUI } from "./write-todos";
+
+// Crypto Tool UI Components - Conversational Crypto Advisor
+export {
+ // Token Analysis
+ TokenAnalysisToolUI,
+ TokenAnalysisArgsSchema,
+ TokenAnalysisResultSchema,
+ type TokenAnalysisArgs,
+ type TokenAnalysisResult,
+ // Watchlist Display
+ WatchlistDisplayToolUI,
+ WatchlistDisplayArgsSchema,
+ WatchlistDisplayResultSchema,
+ type WatchlistDisplayArgs,
+ type WatchlistDisplayResult,
+ // Action Confirmation
+ ActionConfirmationToolUI,
+ ActionConfirmationArgsSchema,
+ ActionConfirmationResultSchema,
+ type ActionConfirmationArgs,
+ type ActionConfirmationResult,
+ // Alert Configuration
+ AlertConfigurationToolUI,
+ AlertConfigurationArgsSchema,
+ AlertConfigurationResultSchema,
+ type AlertConfigurationArgs,
+ type AlertConfigurationResult,
+ // Proactive Alert
+ ProactiveAlertToolUI,
+ ProactiveAlertArgsSchema,
+ ProactiveAlertResultSchema,
+ type ProactiveAlertArgs,
+ type ProactiveAlertResult,
+} from "./crypto";
diff --git a/surfsense_web/lib/mock/cryptoMockData.ts b/surfsense_web/lib/mock/cryptoMockData.ts
new file mode 100644
index 000000000..282351794
--- /dev/null
+++ b/surfsense_web/lib/mock/cryptoMockData.ts
@@ -0,0 +1,467 @@
+/**
+ * Mock data for Crypto Dashboard - SurfSense Web V2
+ * Remove or disable in production
+ */
+
+// ============================================
+// TYPES
+// ============================================
+
+export type ChainType = "solana" | "ethereum" | "base" | "arbitrum" | "polygon" | "bsc";
+
+export interface TokenPrice {
+ symbol: string;
+ name: string;
+ price: number;
+ priceChange24h: number;
+ priceChange7d: number;
+ volume24h: number;
+ marketCap: number;
+ chain: ChainType;
+ icon?: string;
+}
+
+export interface WatchlistToken {
+ id: string;
+ symbol: string;
+ name: string;
+ chain: ChainType;
+ contractAddress: string;
+ price: number;
+ priceChange24h: number;
+ volume24h: number;
+ marketCap: number;
+ liquidity: number;
+ safetyScore: number;
+ hasAlerts: boolean;
+ alertCount?: number;
+ addedAt: Date;
+}
+
+export interface Alert {
+ id: string;
+ tokenSymbol: string;
+ tokenName: string;
+ chain: ChainType;
+ type: "price_above" | "price_below" | "price_change" | "volume" | "whale" | "liquidity" | "safety";
+ message: string;
+ timestamp: Date;
+ isRead: boolean;
+ severity: "info" | "warning" | "critical";
+}
+
+export interface PortfolioToken {
+ id: string;
+ symbol: string;
+ name: string;
+ chain: ChainType;
+ amount: number;
+ avgBuyPrice: number;
+ currentPrice: number;
+ value: number;
+ pnl: number;
+ pnlPercent: number;
+ allocation: number;
+}
+
+export interface PortfolioSummary {
+ totalValue: number;
+ totalPnl: number;
+ totalPnlPercent: number;
+ change24h: number;
+ change24hPercent: number;
+ tokens: PortfolioToken[];
+}
+
+// ============================================
+// MARKET OVERVIEW DATA
+// ============================================
+
+export const MOCK_MARKET_PRICES: TokenPrice[] = [
+ {
+ symbol: "BTC",
+ name: "Bitcoin",
+ price: 97542.18,
+ priceChange24h: 2.34,
+ priceChange7d: 8.12,
+ volume24h: 42_500_000_000,
+ marketCap: 1_920_000_000_000,
+ chain: "ethereum",
+ icon: "βΏ",
+ },
+ {
+ symbol: "ETH",
+ name: "Ethereum",
+ price: 3456.78,
+ priceChange24h: -1.23,
+ priceChange7d: 5.67,
+ volume24h: 18_200_000_000,
+ marketCap: 415_000_000_000,
+ chain: "ethereum",
+ icon: "Ξ",
+ },
+ {
+ symbol: "SOL",
+ name: "Solana",
+ price: 198.45,
+ priceChange24h: 5.67,
+ priceChange7d: 12.34,
+ volume24h: 4_500_000_000,
+ marketCap: 92_000_000_000,
+ chain: "solana",
+ icon: "β",
+ },
+];
+
+export const MOCK_TRENDING_TOKENS: TokenPrice[] = [
+ {
+ symbol: "BULLA",
+ name: "Bulla Token",
+ price: 0.00001234,
+ priceChange24h: 156.7,
+ priceChange7d: 342.5,
+ volume24h: 1_200_000,
+ marketCap: 2_100_000,
+ chain: "solana",
+ },
+ {
+ symbol: "BONK",
+ name: "Bonk",
+ price: 0.00002156,
+ priceChange24h: 12.3,
+ priceChange7d: 45.6,
+ volume24h: 89_000_000,
+ marketCap: 1_450_000_000,
+ chain: "solana",
+ },
+ {
+ symbol: "WIF",
+ name: "dogwifhat",
+ price: 2.45,
+ priceChange24h: -5.2,
+ priceChange7d: 23.4,
+ volume24h: 245_000_000,
+ marketCap: 2_450_000_000,
+ chain: "solana",
+ },
+ {
+ symbol: "PEPE",
+ name: "Pepe",
+ price: 0.00001089,
+ priceChange24h: 8.7,
+ priceChange7d: -12.3,
+ volume24h: 567_000_000,
+ marketCap: 4_580_000_000,
+ chain: "ethereum",
+ },
+];
+
+// ============================================
+// WATCHLIST DATA
+// ============================================
+
+export const MOCK_WATCHLIST: WatchlistToken[] = [
+ {
+ id: "1",
+ symbol: "BULLA",
+ name: "Bulla Token",
+ chain: "solana",
+ contractAddress: "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
+ price: 0.00001234,
+ priceChange24h: 156.7,
+ volume24h: 1_200_000,
+ marketCap: 2_100_000,
+ liquidity: 450_000,
+ safetyScore: 72,
+ hasAlerts: true,
+ alertCount: 2,
+ addedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3),
+ },
+ {
+ id: "2",
+ symbol: "BONK",
+ name: "Bonk",
+ chain: "solana",
+ contractAddress: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
+ price: 0.00002156,
+ priceChange24h: 12.3,
+ volume24h: 89_000_000,
+ marketCap: 1_450_000_000,
+ liquidity: 45_000_000,
+ safetyScore: 89,
+ hasAlerts: true,
+ alertCount: 1,
+ addedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7),
+ },
+ {
+ id: "3",
+ symbol: "WIF",
+ name: "dogwifhat",
+ chain: "solana",
+ contractAddress: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm",
+ price: 2.45,
+ priceChange24h: -5.2,
+ volume24h: 245_000_000,
+ marketCap: 2_450_000_000,
+ liquidity: 125_000_000,
+ safetyScore: 94,
+ hasAlerts: false,
+ addedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 14),
+ },
+ {
+ id: "4",
+ symbol: "PEPE",
+ name: "Pepe",
+ chain: "ethereum",
+ contractAddress: "0x6982508145454Ce325dDbE47a25d4ec3d2311933",
+ price: 0.00001089,
+ priceChange24h: 8.7,
+ volume24h: 567_000_000,
+ marketCap: 4_580_000_000,
+ liquidity: 234_000_000,
+ safetyScore: 85,
+ hasAlerts: false,
+ addedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30),
+ },
+ {
+ id: "5",
+ symbol: "DEGEN",
+ name: "Degen",
+ chain: "base",
+ contractAddress: "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed",
+ price: 0.0156,
+ priceChange24h: -15.3,
+ volume24h: 12_000_000,
+ marketCap: 156_000_000,
+ liquidity: 8_500_000,
+ safetyScore: 78,
+ hasAlerts: true,
+ alertCount: 3,
+ addedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5),
+ },
+];
+
+// ============================================
+// ALERTS DATA
+// ============================================
+
+export const MOCK_ALERTS: Alert[] = [
+ {
+ id: "alert-1",
+ tokenSymbol: "BULLA",
+ tokenName: "Bulla Token",
+ chain: "solana",
+ type: "price_above",
+ message: "BULLA price increased above $0.00001 (+156%)",
+ timestamp: new Date(Date.now() - 1000 * 60 * 5),
+ isRead: false,
+ severity: "info",
+ },
+ {
+ id: "alert-2",
+ tokenSymbol: "BULLA",
+ tokenName: "Bulla Token",
+ chain: "solana",
+ type: "whale",
+ message: "Large transaction detected: 500M BULLA ($6,170) transferred",
+ timestamp: new Date(Date.now() - 1000 * 60 * 15),
+ isRead: false,
+ severity: "warning",
+ },
+ {
+ id: "alert-3",
+ tokenSymbol: "DEGEN",
+ tokenName: "Degen",
+ chain: "base",
+ type: "price_below",
+ message: "DEGEN dropped below $0.02 (-15%)",
+ timestamp: new Date(Date.now() - 1000 * 60 * 30),
+ isRead: false,
+ severity: "critical",
+ },
+ {
+ id: "alert-4",
+ tokenSymbol: "BONK",
+ tokenName: "Bonk",
+ chain: "solana",
+ type: "volume",
+ message: "BONK volume spike: 3x average in last hour",
+ timestamp: new Date(Date.now() - 1000 * 60 * 60),
+ isRead: true,
+ severity: "info",
+ },
+ {
+ id: "alert-5",
+ tokenSymbol: "DEGEN",
+ tokenName: "Degen",
+ chain: "base",
+ type: "liquidity",
+ message: "DEGEN liquidity decreased by 12% ($1.2M removed)",
+ timestamp: new Date(Date.now() - 1000 * 60 * 120),
+ isRead: true,
+ severity: "warning",
+ },
+];
+
+// ============================================
+// PORTFOLIO DATA
+// ============================================
+
+export const MOCK_PORTFOLIO: PortfolioSummary = {
+ totalValue: 15_234.56,
+ totalPnl: 3_456.78,
+ totalPnlPercent: 29.34,
+ change24h: 456.12,
+ change24hPercent: 3.08,
+ tokens: [
+ {
+ id: "p1",
+ symbol: "SOL",
+ name: "Solana",
+ chain: "solana",
+ amount: 25.5,
+ avgBuyPrice: 145.00,
+ currentPrice: 198.45,
+ value: 5_060.48,
+ pnl: 1_362.98,
+ pnlPercent: 36.86,
+ allocation: 33.2,
+ },
+ {
+ id: "p2",
+ symbol: "ETH",
+ name: "Ethereum",
+ chain: "ethereum",
+ amount: 1.2,
+ avgBuyPrice: 2_800.00,
+ currentPrice: 3_456.78,
+ value: 4_148.14,
+ pnl: 788.14,
+ pnlPercent: 23.46,
+ allocation: 27.2,
+ },
+ {
+ id: "p3",
+ symbol: "BONK",
+ name: "Bonk",
+ chain: "solana",
+ amount: 150_000_000,
+ avgBuyPrice: 0.000015,
+ currentPrice: 0.00002156,
+ value: 3_234.00,
+ pnl: 984.00,
+ pnlPercent: 43.73,
+ allocation: 21.2,
+ },
+ {
+ id: "p4",
+ symbol: "WIF",
+ name: "dogwifhat",
+ chain: "solana",
+ amount: 500,
+ avgBuyPrice: 1.80,
+ currentPrice: 2.45,
+ value: 1_225.00,
+ pnl: 325.00,
+ pnlPercent: 36.11,
+ allocation: 8.0,
+ },
+ {
+ id: "p5",
+ symbol: "PEPE",
+ name: "Pepe",
+ chain: "ethereum",
+ amount: 100_000_000,
+ avgBuyPrice: 0.000012,
+ currentPrice: 0.00001089,
+ value: 1_089.00,
+ pnl: -111.00,
+ pnlPercent: -9.25,
+ allocation: 7.2,
+ },
+ {
+ id: "p6",
+ symbol: "DEGEN",
+ name: "Degen",
+ chain: "base",
+ amount: 30_000,
+ avgBuyPrice: 0.012,
+ currentPrice: 0.0156,
+ value: 468.00,
+ pnl: 108.00,
+ pnlPercent: 30.00,
+ allocation: 3.1,
+ },
+ ],
+};
+
+// ============================================
+// HELPER FUNCTIONS
+// ============================================
+
+export function formatPrice(price: number): string {
+ if (price >= 1000) {
+ return `$${price.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
+ } else if (price >= 1) {
+ return `$${price.toFixed(2)}`;
+ } else if (price >= 0.0001) {
+ return `$${price.toFixed(6)}`;
+ } else {
+ return `$${price.toFixed(10)}`;
+ }
+}
+
+export function formatLargeNumber(num: number): string {
+ if (num >= 1_000_000_000) {
+ return `$${(num / 1_000_000_000).toFixed(2)}B`;
+ } else if (num >= 1_000_000) {
+ return `$${(num / 1_000_000).toFixed(2)}M`;
+ } else if (num >= 1_000) {
+ return `$${(num / 1_000).toFixed(2)}K`;
+ }
+ return `$${num.toFixed(2)}`;
+}
+
+export function formatPercent(percent: number): string {
+ const sign = percent >= 0 ? "+" : "";
+ return `${sign}${percent.toFixed(2)}%`;
+}
+
+export function getChainColor(chain: ChainType): string {
+ const colors: Record = {
+ solana: "#9945FF",
+ ethereum: "#627EEA",
+ base: "#0052FF",
+ arbitrum: "#28A0F0",
+ polygon: "#8247E5",
+ bsc: "#F0B90B",
+ };
+ return colors[chain] || "#888888";
+}
+
+export function getChainName(chain: ChainType): string {
+ const names: Record = {
+ solana: "Solana",
+ ethereum: "Ethereum",
+ base: "Base",
+ arbitrum: "Arbitrum",
+ polygon: "Polygon",
+ bsc: "BNB Chain",
+ };
+ return names[chain] || chain;
+}
+
+export function getSafetyColor(score: number): string {
+ if (score >= 80) return "#22C55E"; // green
+ if (score >= 60) return "#EAB308"; // yellow
+ if (score >= 40) return "#F97316"; // orange
+ return "#EF4444"; // red
+}
+
+export function getSafetyLabel(score: number): string {
+ if (score >= 80) return "Safe";
+ if (score >= 60) return "Medium";
+ if (score >= 40) return "Risky";
+ return "Danger";
+}
+
diff --git a/surfsense_web/start-dev.sh b/surfsense_web/start-dev.sh
new file mode 100755
index 000000000..62ad72ff9
--- /dev/null
+++ b/surfsense_web/start-dev.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+cd /Users/mac_1/Documents/GitHub/SurfSense/surfsense_web
+pnpm dev
+