feat(crypto): add SurfSense 2.0 Crypto Co-Pilot UI components

Frontend - Web Dashboard:
- Add crypto dashboard page with Watchlist, Alerts, Market, Profile tabs
- Add 11 tool-ui components for inline chat display
- Add crypto components (ChainIcon, SafetyBadge, PriceDisplay, etc.)
- Add modals (AddTokenModal, CreateAlertModal)
- Add mock data for development

Frontend - Browser Extension:
- Add shared components (ChainIcon, RiskBadge, PriceDisplay, SuggestionCard)
- Add crypto components (SafetyScoreDisplay, WatchlistPanel, AlertConfigModal)
- Add chat enhancements (WelcomeScreen, ThinkingStepsDisplay)
- Add widget components for inline display
- Enhance TokenInfoCard, ChatHeader, ChatInput, ChatInterface

Documentation:
- Add conversational UX specification
- Add UX analysis report
- Update extension UX design

This implements the Conversational UX paradigm where crypto features
are AI-callable tools that render inline in the chat interface.
This commit is contained in:
API Test Bot 2026-02-04 02:19:57 +07:00
parent ad795eb830
commit e4d020799b
58 changed files with 11315 additions and 661 deletions

View file

@ -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 (
<div className={cn(
"rounded-lg border bg-card p-4 my-2",
className
)}>
{/* Header */}
<div className="flex items-center gap-2 mb-3">
<div className="w-8 h-8 rounded-full bg-green-500/10 flex items-center justify-center">
<CheckCircle className="h-4 w-4 text-green-500" />
</div>
<span className="font-medium text-sm">Action Confirmed</span>
</div>
{/* Action details */}
<div className="bg-muted/50 rounded-md p-3 mb-3">
<div className="flex items-center gap-2 mb-2">
<Icon className="h-4 w-4 text-primary" />
<span className="font-medium text-sm">{getActionTitle()}</span>
</div>
{details.length > 0 && (
<div className="space-y-1 mt-2">
<p className="text-xs text-muted-foreground">Also set up:</p>
{details.map((detail, index) => (
<div key={index} className="flex items-center gap-2 text-xs">
<Bell className="h-3 w-3 text-primary" />
<span>{detail}</span>
</div>
))}
</div>
)}
</div>
{/* Action buttons */}
<div className="flex gap-2">
{(actionType === "watchlist_add" || actionType === "watchlist_remove") && (
<Button
size="sm"
variant="outline"
onClick={onViewWatchlist}
className="flex-1"
>
<Eye className="h-3 w-3 mr-1" />
View Watchlist
</Button>
)}
{(actionType === "watchlist_add" || actionType === "alert_set") && (
<Button
size="sm"
variant="outline"
onClick={onEditAlerts}
className="flex-1"
>
<Settings className="h-3 w-3 mr-1" />
Edit Alerts
</Button>
)}
</div>
</div>
);
}

View file

@ -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 (
<div className={cn(
"rounded-lg border bg-card p-4 my-2",
className
)}>
{/* Header */}
<div className="flex items-center gap-2 mb-3">
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
{isNew ? (
<CheckCircle className="h-4 w-4 text-green-500" />
) : (
<Bell className="h-4 w-4 text-primary" />
)}
</div>
<span className="font-medium text-sm">
{isNew ? "Alert Created" : "AlertWidget"}
</span>
</div>
{/* Alert details */}
<div className="bg-muted/50 rounded-md p-3 mb-3 space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Token:</span>
<span className="font-medium">{config.tokenSymbol}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Condition:</span>
<span className="font-medium">{config.condition}</span>
</div>
{config.currentPrice && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Current:</span>
<span>{config.currentPrice}</span>
</div>
)}
{config.triggerPrice && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Trigger at:</span>
<span className="font-medium text-primary">{config.triggerPrice}</span>
</div>
)}
{/* Notification channels */}
<div className="pt-2 border-t">
<p className="text-xs text-muted-foreground mb-1">Notify via:</p>
<div className="flex flex-wrap gap-2">
<span className={cn(
"text-xs px-2 py-0.5 rounded",
config.channels.browser ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground line-through"
)}>
{config.channels.browser ? "✓" : "✗"} Browser
</span>
<span className={cn(
"text-xs px-2 py-0.5 rounded",
config.channels.inApp ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground line-through"
)}>
{config.channels.inApp ? "✓" : "✗"} In-app
</span>
<span className={cn(
"text-xs px-2 py-0.5 rounded",
config.channels.email ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground line-through"
)}>
{config.channels.email ? "✓" : "✗"} Email
</span>
</div>
</div>
</div>
{/* Action buttons */}
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={onEdit} className="flex-1">
<Edit className="h-3 w-3 mr-1" />
Edit
</Button>
<Button size="sm" variant="outline" onClick={onDelete}>
<Trash2 className="h-3 w-3" />
</Button>
<Button size="sm" variant="outline" onClick={onAddAnother}>
<Plus className="h-3 w-3" />
</Button>
</div>
{/* View all link */}
{onViewAll && (
<button
onClick={onViewAll}
className="w-full mt-2 text-xs text-primary hover:underline"
>
View all alerts
</button>
)}
</div>
);
}

View file

@ -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 (
<div className={cn(
"rounded-lg border bg-card p-4 my-2",
className
)}>
{/* Header */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className={cn("w-8 h-8 rounded-full flex items-center justify-center", colorClass)}>
<Icon className="h-4 w-4" />
</div>
<div>
<span className="font-medium text-sm">🚨 ProactiveAlertCard</span>
<p className="text-xs text-muted-foreground">
{alert.timestamp.toLocaleTimeString()}
</p>
</div>
</div>
<button
onClick={onDismiss}
className="p-1 hover:bg-muted rounded text-muted-foreground"
>
<X className="h-4 w-4" />
</button>
</div>
{/* Alert content */}
<div className="bg-muted/50 rounded-md p-3 mb-3">
<p className="font-medium text-sm mb-2">{alert.title}</p>
{/* Price info */}
{(alert.currentPrice || alert.entryPrice || alert.pnl) && (
<div className="space-y-1 text-xs">
{alert.currentPrice && (
<div className="flex justify-between">
<span className="text-muted-foreground">📊 Current:</span>
<span className="font-medium">{alert.currentPrice}</span>
</div>
)}
{alert.entryPrice && (
<div className="flex justify-between">
<span className="text-muted-foreground">📈 Your entry:</span>
<span>{alert.entryPrice}</span>
</div>
)}
{alert.pnl && (
<div className="flex justify-between">
<span className="text-muted-foreground">💰 Your P&L:</span>
<span className={cn(
"font-medium",
alert.pnl.startsWith("+") ? "text-green-500" : "text-red-500"
)}>{alert.pnl}</span>
</div>
)}
</div>
)}
{/* Warnings */}
{alert.warnings && alert.warnings.length > 0 && (
<div className="mt-2 pt-2 border-t space-y-1">
{alert.warnings.map((warning, index) => (
<div key={index} className="flex items-center gap-2 text-xs text-yellow-600">
<AlertTriangle className="h-3 w-3" />
<span>{warning}</span>
</div>
))}
</div>
)}
</div>
{/* AI Recommendation */}
{recommendation && (
<p className="text-sm text-muted-foreground mb-3 italic">
{recommendation}
</p>
)}
{/* Action buttons */}
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={onTellMore} className="flex-1">
<Info className="h-3 w-3 mr-1" />
Tell me more
</Button>
<Button size="sm" variant="outline" onClick={onViewDetails}>
<ChevronRight className="h-3 w-3" />
</Button>
<Button size="sm" variant="outline" onClick={onSetAlert}>
<Bell className="h-3 w-3" />
</Button>
</div>
</div>
);
}

View file

@ -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 (
<div className={cn(
"rounded-lg border bg-card p-4 my-2",
className
)}>
{/* Header */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className="text-lg">📊</span>
<span className="font-medium text-sm">TokenAnalysisCard</span>
</div>
<ChainIcon chain={data.chain} size="sm" />
</div>
{/* Token info */}
<div className="flex items-center gap-3 mb-3 pb-3 border-b">
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<span className="text-lg">🪙</span>
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-bold">{data.symbol}</span>
{data.name && (
<span className="text-xs text-muted-foreground">{data.name}</span>
)}
</div>
<div className="flex items-center gap-2">
<span className="font-medium">{data.price}</span>
<span className={cn(
"flex items-center gap-0.5 text-sm",
data.priceChange24h >= 0 ? "text-green-500" : "text-red-500"
)}>
{data.priceChange24h >= 0 ? (
<TrendingUp className="h-3 w-3" />
) : (
<TrendingDown className="h-3 w-3" />
)}
{data.priceChange24h >= 0 ? "+" : ""}{data.priceChange24h.toFixed(1)}%
</span>
</div>
</div>
<button
onClick={onAddToWatchlist}
className={cn(
"p-2 rounded-full transition-colors",
isInWatchlist
? "text-yellow-500 bg-yellow-500/10"
: "text-muted-foreground hover:text-yellow-500 hover:bg-yellow-500/10"
)}
>
<Star className={cn("h-5 w-5", isInWatchlist && "fill-current")} />
</button>
</div>
{/* Metrics grid */}
<div className="grid grid-cols-2 gap-2 mb-3 text-sm">
{data.marketCap && (
<div className="bg-muted/50 rounded p-2">
<p className="text-xs text-muted-foreground">Market Cap</p>
<p className="font-medium">{data.marketCap}</p>
</div>
)}
{data.volume24h && (
<div className="bg-muted/50 rounded p-2">
<p className="text-xs text-muted-foreground">24h Volume</p>
<p className="font-medium">{data.volume24h}</p>
</div>
)}
{data.liquidity && (
<div className="bg-muted/50 rounded p-2">
<p className="text-xs text-muted-foreground">Liquidity</p>
<p className="font-medium">{data.liquidity}</p>
</div>
)}
{data.safetyScore !== undefined && (
<div className="bg-muted/50 rounded p-2">
<p className="text-xs text-muted-foreground">Safety Score</p>
<p className={cn("font-medium flex items-center gap-1", getSafetyColor(data.safetyScore))}>
<Shield className="h-3 w-3" />
{data.safetyScore}/100 ({getSafetyLabel(data.safetyScore)})
</p>
</div>
)}
</div>
{/* Holder info */}
{(data.holderCount || data.top10HolderPercent) && (
<div className="flex items-center gap-4 mb-3 text-xs text-muted-foreground">
{data.holderCount && (
<span className="flex items-center gap-1">
<Users className="h-3 w-3" />
{data.holderCount.toLocaleString()} holders
</span>
)}
{data.top10HolderPercent && (
<span className={cn(
"flex items-center gap-1",
data.top10HolderPercent > 50 ? "text-yellow-500" : ""
)}>
{data.top10HolderPercent > 50 && <AlertTriangle className="h-3 w-3" />}
Top 10: {data.top10HolderPercent}%
</span>
)}
</div>
)}
{/* Action buttons */}
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={onAddToWatchlist} className="flex-1">
<Star className="h-3 w-3 mr-1" />
{isInWatchlist ? "In Watchlist" : "Add to Watchlist"}
</Button>
<Button size="sm" variant="outline" onClick={onSetAlert}>
<Bell className="h-3 w-3" />
</Button>
<Button size="sm" variant="default" onClick={onAnalyzeFurther}>
Analyze More
</Button>
</div>
</div>
);
}

View file

@ -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 (
<div className={cn(
"rounded-lg border bg-card p-4 my-2",
className
)}>
<div className="flex items-center gap-2 mb-3">
<span className="text-lg">📋</span>
<span className="font-medium text-sm">Your Watchlist</span>
</div>
<div className="text-center py-4 text-muted-foreground text-sm">
Your watchlist is empty. Add tokens to track them!
</div>
<Button size="sm" variant="outline" onClick={onAddToken} className="w-full">
<Plus className="h-3 w-3 mr-1" />
Add Token
</Button>
</div>
);
}
return (
<div className={cn(
"rounded-lg border bg-card p-4 my-2",
className
)}>
{/* Header */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className="text-lg">📋</span>
<span className="font-medium text-sm">WatchlistWidget</span>
</div>
<span className="text-xs text-muted-foreground">{tokens.length} tokens</span>
</div>
{/* Token list */}
<div className="space-y-2 mb-3">
{tokens.map((token) => (
<div
key={token.id}
className="flex items-center gap-3 p-2 rounded-md bg-muted/50 hover:bg-muted transition-colors"
>
{/* Token info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{token.symbol}</span>
<ChainIcon chain={token.chain} size="sm" />
{token.alertCount && token.alertCount > 0 && (
<span className="flex items-center gap-0.5 text-xs text-primary">
<Bell className="h-3 w-3" />
{token.alertCount}
</span>
)}
</div>
<p className="text-xs text-muted-foreground">{token.price}</p>
</div>
{/* Price change */}
<div className={cn(
"flex items-center gap-1 text-xs font-medium",
token.priceChange24h >= 0 ? "text-green-500" : "text-red-500"
)}>
{token.priceChange24h >= 0 ? (
<TrendingUp className="h-3 w-3" />
) : (
<TrendingDown className="h-3 w-3" />
)}
{token.priceChange24h >= 0 ? "+" : ""}{token.priceChange24h.toFixed(1)}%
</div>
{/* Actions */}
<div className="flex gap-1">
<button
className="p-1 hover:bg-background rounded text-muted-foreground hover:text-foreground"
onClick={() => onAnalyze?.(token)}
title="Analyze"
>
<Search className="h-3 w-3" />
</button>
<button
className="p-1 hover:bg-destructive/10 rounded text-muted-foreground hover:text-destructive"
onClick={() => onRemove?.(token.id)}
title="Remove"
>
<Trash2 className="h-3 w-3" />
</button>
</div>
</div>
))}
</div>
{/* Footer actions */}
<div className="flex gap-2 pt-2 border-t">
<Button size="sm" variant="outline" onClick={onAddToken} className="flex-1">
<Plus className="h-3 w-3 mr-1" />
Add Token
</Button>
{tokens.length > 0 && (
<Button size="sm" variant="ghost" onClick={onClearAll} className="text-destructive">
Clear All
</Button>
)}
</div>
</div>
);
}

View file

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