mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-28 10:26:33 +02:00
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:
parent
ad795eb830
commit
e4d020799b
58 changed files with 11315 additions and 661 deletions
|
|
@ -0,0 +1,129 @@
|
|||
import { cn } from "~/lib/utils";
|
||||
|
||||
export type ChainType = "solana" | "ethereum" | "base" | "arbitrum" | "polygon" | "bsc" | "avalanche" | "unknown";
|
||||
|
||||
export interface ChainIconProps {
|
||||
/** Blockchain chain identifier */
|
||||
chain: ChainType | string;
|
||||
/** Size of the icon */
|
||||
size?: "sm" | "md" | "lg";
|
||||
/** Show chain name label */
|
||||
showLabel?: boolean;
|
||||
/** Additional class names */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Chain configuration with colors and display names
|
||||
const CHAIN_CONFIG: Record<string, { color: string; bgColor: string; label: string; emoji: string }> = {
|
||||
solana: {
|
||||
color: "#9945FF",
|
||||
bgColor: "bg-purple-500/10",
|
||||
label: "Solana",
|
||||
emoji: "◎",
|
||||
},
|
||||
ethereum: {
|
||||
color: "#627EEA",
|
||||
bgColor: "bg-blue-500/10",
|
||||
label: "Ethereum",
|
||||
emoji: "Ξ",
|
||||
},
|
||||
base: {
|
||||
color: "#0052FF",
|
||||
bgColor: "bg-blue-600/10",
|
||||
label: "Base",
|
||||
emoji: "🔵",
|
||||
},
|
||||
arbitrum: {
|
||||
color: "#28A0F0",
|
||||
bgColor: "bg-sky-500/10",
|
||||
label: "Arbitrum",
|
||||
emoji: "🔷",
|
||||
},
|
||||
polygon: {
|
||||
color: "#8247E5",
|
||||
bgColor: "bg-violet-500/10",
|
||||
label: "Polygon",
|
||||
emoji: "⬡",
|
||||
},
|
||||
bsc: {
|
||||
color: "#F0B90B",
|
||||
bgColor: "bg-yellow-500/10",
|
||||
label: "BSC",
|
||||
emoji: "🟡",
|
||||
},
|
||||
avalanche: {
|
||||
color: "#E84142",
|
||||
bgColor: "bg-red-500/10",
|
||||
label: "Avalanche",
|
||||
emoji: "🔺",
|
||||
},
|
||||
unknown: {
|
||||
color: "#6B7280",
|
||||
bgColor: "bg-gray-500/10",
|
||||
label: "Unknown",
|
||||
emoji: "🔗",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* ChainIcon - Displays blockchain chain icon with optional label
|
||||
*
|
||||
* Features:
|
||||
* - Chain-specific colors and icons
|
||||
* - Multiple size variants
|
||||
* - Optional chain name label
|
||||
*/
|
||||
export function ChainIcon({
|
||||
chain,
|
||||
size = "md",
|
||||
showLabel = false,
|
||||
className,
|
||||
}: ChainIconProps) {
|
||||
const normalizedChain = chain.toLowerCase();
|
||||
const config = CHAIN_CONFIG[normalizedChain] || CHAIN_CONFIG.unknown;
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "w-4 h-4 text-xs",
|
||||
md: "w-5 h-5 text-sm",
|
||||
lg: "w-6 h-6 text-base",
|
||||
};
|
||||
|
||||
const labelSizes = {
|
||||
sm: "text-xs",
|
||||
md: "text-sm",
|
||||
lg: "text-base",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1.5", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-full flex items-center justify-center",
|
||||
config.bgColor,
|
||||
sizeClasses[size]
|
||||
)}
|
||||
style={{ color: config.color }}
|
||||
title={config.label}
|
||||
>
|
||||
<span>{config.emoji}</span>
|
||||
</div>
|
||||
{showLabel && (
|
||||
<span
|
||||
className={cn("font-medium", labelSizes[size])}
|
||||
style={{ color: config.color }}
|
||||
>
|
||||
{config.label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chain color for custom styling
|
||||
*/
|
||||
export function getChainColor(chain: string): string {
|
||||
const normalizedChain = chain.toLowerCase();
|
||||
return CHAIN_CONFIG[normalizedChain]?.color || CHAIN_CONFIG.unknown.color;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import { cn } from "~/lib/utils";
|
||||
import { TrendingUp, TrendingDown, Minus } from "lucide-react";
|
||||
|
||||
export interface PriceDisplayProps {
|
||||
/** Current price value */
|
||||
price: string | number;
|
||||
/** Price change percentage (positive = up, negative = down) */
|
||||
priceChange?: number;
|
||||
/** Show the change indicator arrow */
|
||||
showChangeIndicator?: boolean;
|
||||
/** Size variant */
|
||||
size?: "sm" | "md" | "lg";
|
||||
/** Additional class names */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* PriceDisplay - Shows price with optional change indicator
|
||||
*
|
||||
* Features:
|
||||
* - Color-coded price changes (green for up, red for down)
|
||||
* - Animated arrow indicators
|
||||
* - Multiple size variants
|
||||
*/
|
||||
export function PriceDisplay({
|
||||
price,
|
||||
priceChange,
|
||||
showChangeIndicator = true,
|
||||
size = "md",
|
||||
className,
|
||||
}: PriceDisplayProps) {
|
||||
const isPositive = priceChange !== undefined && priceChange > 0;
|
||||
const isNegative = priceChange !== undefined && priceChange < 0;
|
||||
const isNeutral = priceChange === undefined || priceChange === 0;
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "text-sm",
|
||||
md: "text-base",
|
||||
lg: "text-xl font-semibold",
|
||||
};
|
||||
|
||||
const changeClasses = {
|
||||
sm: "text-xs",
|
||||
md: "text-sm",
|
||||
lg: "text-base",
|
||||
};
|
||||
|
||||
const iconSizes = {
|
||||
sm: "h-3 w-3",
|
||||
md: "h-4 w-4",
|
||||
lg: "h-5 w-5",
|
||||
};
|
||||
|
||||
const formatPrice = (value: string | number): string => {
|
||||
if (typeof value === "string") return value;
|
||||
if (value < 0.00001) return `$${value.toExponential(2)}`;
|
||||
if (value < 1) return `$${value.toFixed(6)}`;
|
||||
if (value < 1000) return `$${value.toFixed(2)}`;
|
||||
return `$${value.toLocaleString(undefined, { maximumFractionDigits: 2 })}`;
|
||||
};
|
||||
|
||||
const formatChange = (change: number): string => {
|
||||
const sign = change > 0 ? "+" : "";
|
||||
return `${sign}${change.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2", className)}>
|
||||
{/* Price */}
|
||||
<span className={cn("font-medium", sizeClasses[size])}>
|
||||
{formatPrice(price)}
|
||||
</span>
|
||||
|
||||
{/* Change indicator */}
|
||||
{showChangeIndicator && priceChange !== undefined && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-0.5",
|
||||
changeClasses[size],
|
||||
isPositive && "text-green-500",
|
||||
isNegative && "text-red-500",
|
||||
isNeutral && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{isPositive && <TrendingUp className={iconSizes[size]} />}
|
||||
{isNegative && <TrendingDown className={iconSizes[size]} />}
|
||||
{isNeutral && <Minus className={iconSizes[size]} />}
|
||||
<span>{formatChange(priceChange)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import { cn } from "~/lib/utils";
|
||||
import { Shield, AlertTriangle, XCircle, CheckCircle } from "lucide-react";
|
||||
|
||||
export type RiskLevel = "safe" | "low" | "medium" | "high" | "critical";
|
||||
|
||||
export interface RiskBadgeProps {
|
||||
/** Risk level */
|
||||
level: RiskLevel;
|
||||
/** Optional score (0-100) */
|
||||
score?: number;
|
||||
/** Show score value */
|
||||
showScore?: boolean;
|
||||
/** Size variant */
|
||||
size?: "sm" | "md" | "lg";
|
||||
/** Additional class names */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Risk level configuration
|
||||
const RISK_CONFIG: Record<RiskLevel, {
|
||||
label: string;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
borderColor: string;
|
||||
Icon: typeof Shield;
|
||||
}> = {
|
||||
safe: {
|
||||
label: "Safe",
|
||||
color: "text-green-600 dark:text-green-400",
|
||||
bgColor: "bg-green-500/10",
|
||||
borderColor: "border-green-500/30",
|
||||
Icon: CheckCircle,
|
||||
},
|
||||
low: {
|
||||
label: "Low Risk",
|
||||
color: "text-green-500 dark:text-green-400",
|
||||
bgColor: "bg-green-500/10",
|
||||
borderColor: "border-green-500/30",
|
||||
Icon: Shield,
|
||||
},
|
||||
medium: {
|
||||
label: "Medium",
|
||||
color: "text-yellow-600 dark:text-yellow-400",
|
||||
bgColor: "bg-yellow-500/10",
|
||||
borderColor: "border-yellow-500/30",
|
||||
Icon: AlertTriangle,
|
||||
},
|
||||
high: {
|
||||
label: "High Risk",
|
||||
color: "text-orange-600 dark:text-orange-400",
|
||||
bgColor: "bg-orange-500/10",
|
||||
borderColor: "border-orange-500/30",
|
||||
Icon: AlertTriangle,
|
||||
},
|
||||
critical: {
|
||||
label: "Critical",
|
||||
color: "text-red-600 dark:text-red-400",
|
||||
bgColor: "bg-red-500/10",
|
||||
borderColor: "border-red-500/30",
|
||||
Icon: XCircle,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* RiskBadge - Displays risk level with color-coded badge
|
||||
*
|
||||
* Features:
|
||||
* - Color-coded risk levels (safe to critical)
|
||||
* - Optional score display
|
||||
* - Multiple size variants
|
||||
* - Accessible with proper ARIA labels
|
||||
*/
|
||||
export function RiskBadge({
|
||||
level,
|
||||
score,
|
||||
showScore = false,
|
||||
size = "md",
|
||||
className,
|
||||
}: RiskBadgeProps) {
|
||||
const config = RISK_CONFIG[level];
|
||||
const { Icon } = config;
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "px-1.5 py-0.5 text-xs gap-1",
|
||||
md: "px-2 py-1 text-sm gap-1.5",
|
||||
lg: "px-3 py-1.5 text-base gap-2",
|
||||
};
|
||||
|
||||
const iconSizes = {
|
||||
sm: "h-3 w-3",
|
||||
md: "h-4 w-4",
|
||||
lg: "h-5 w-5",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-full border font-medium",
|
||||
config.bgColor,
|
||||
config.borderColor,
|
||||
config.color,
|
||||
sizeClasses[size],
|
||||
className
|
||||
)}
|
||||
role="status"
|
||||
aria-label={`Risk level: ${config.label}${score !== undefined ? `, Score: ${score}` : ""}`}
|
||||
>
|
||||
<Icon className={iconSizes[size]} />
|
||||
<span>{config.label}</span>
|
||||
{showScore && score !== undefined && (
|
||||
<span className="font-bold">({score})</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get risk level from score (0-100)
|
||||
*/
|
||||
export function getRiskLevelFromScore(score: number): RiskLevel {
|
||||
if (score >= 80) return "safe";
|
||||
if (score >= 60) return "low";
|
||||
if (score >= 40) return "medium";
|
||||
if (score >= 20) return "high";
|
||||
return "critical";
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import { cn } from "~/lib/utils";
|
||||
import { ArrowRight, Sparkles, TrendingUp, Shield, Wallet } from "lucide-react";
|
||||
|
||||
export type SuggestionType = "general" | "safety" | "trending" | "wallet" | "custom";
|
||||
|
||||
export interface SuggestionCardProps {
|
||||
/** Suggestion text to display */
|
||||
text: string;
|
||||
/** Type of suggestion for icon selection */
|
||||
type?: SuggestionType;
|
||||
/** Custom icon (overrides type icon) */
|
||||
icon?: React.ReactNode;
|
||||
/** Click handler */
|
||||
onClick?: (text: string) => void;
|
||||
/** Disabled state */
|
||||
disabled?: boolean;
|
||||
/** Additional class names */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Suggestion type icons
|
||||
const TYPE_ICONS: Record<SuggestionType, typeof Sparkles> = {
|
||||
general: Sparkles,
|
||||
safety: Shield,
|
||||
trending: TrendingUp,
|
||||
wallet: Wallet,
|
||||
custom: Sparkles,
|
||||
};
|
||||
|
||||
/**
|
||||
* SuggestionCard - Clickable suggestion card for chat prompts
|
||||
*
|
||||
* Features:
|
||||
* - Type-specific icons
|
||||
* - Hover animations
|
||||
* - Click to send suggestion
|
||||
* - Accessible keyboard navigation
|
||||
*/
|
||||
export function SuggestionCard({
|
||||
text,
|
||||
type = "general",
|
||||
icon,
|
||||
onClick,
|
||||
disabled = false,
|
||||
className,
|
||||
}: SuggestionCardProps) {
|
||||
const Icon = TYPE_ICONS[type];
|
||||
|
||||
const handleClick = () => {
|
||||
if (!disabled && onClick) {
|
||||
onClick(text);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={cn(
|
||||
"group flex items-center gap-3 p-3 rounded-lg border",
|
||||
"bg-card hover:bg-accent/50 transition-all duration-200",
|
||||
"cursor-pointer select-none",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
disabled && "opacity-50 cursor-not-allowed",
|
||||
className
|
||||
)}
|
||||
aria-disabled={disabled}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary">
|
||||
{icon || <Icon className="h-4 w-4" />}
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<span className="flex-1 text-sm text-foreground line-clamp-2">
|
||||
{text}
|
||||
</span>
|
||||
|
||||
{/* Arrow indicator */}
|
||||
<ArrowRight
|
||||
className={cn(
|
||||
"h-4 w-4 text-muted-foreground",
|
||||
"opacity-0 -translate-x-2 transition-all duration-200",
|
||||
"group-hover:opacity-100 group-hover:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default crypto-related suggestions
|
||||
*/
|
||||
export const DEFAULT_CRYPTO_SUGGESTIONS: Array<{ text: string; type: SuggestionType }> = [
|
||||
{ text: "Is this token safe to invest in?", type: "safety" },
|
||||
{ text: "What are the top gainers on Solana today?", type: "trending" },
|
||||
{ text: "Analyze this wallet's trading history", type: "wallet" },
|
||||
{ text: "Check for rug pull indicators", type: "safety" },
|
||||
{ text: "What's the market sentiment for this token?", type: "general" },
|
||||
];
|
||||
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
// Shared components for SurfSense Browser Extension
|
||||
// These components are reusable across the extension UI
|
||||
|
||||
export { PriceDisplay, type PriceDisplayProps } from "./PriceDisplay";
|
||||
export { ChainIcon, getChainColor, type ChainIconProps, type ChainType } from "./ChainIcon";
|
||||
export { RiskBadge, getRiskLevelFromScore, type RiskBadgeProps, type RiskLevel } from "./RiskBadge";
|
||||
export { SuggestionCard, DEFAULT_CRYPTO_SUGGESTIONS, type SuggestionCardProps, type SuggestionType } from "./SuggestionCard";
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue