mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-30 11:26:24 +02:00
feat(detection): implement multi-page token detection system
- Add extractTwitterTokens() to detect $TOKEN mentions (e.g., $BONK, $SOL) - Add extractContractAddresses() for Solana (base58) and Ethereum (0x) addresses - Add extractTradingPairs() to detect TOKEN/SOL, TOKEN/USDT patterns - Update extractPageContext() to use new detection functions - Add detectedTokens field to PageContext interface - Create DetectedTokensList component to display detected tokens - Integrate DetectedTokensList into ChatInterface - Add handleDetectedTokenClick to analyze selected tokens - Support auto-detection on Twitter, generic pages, and DexScreener Implements Task 2: Multi-Page Token Detection Part of hybrid token detection system (manual search + auto-detect)
This commit is contained in:
parent
cb879fca37
commit
e89824db0f
4 changed files with 251 additions and 0 deletions
|
|
@ -82,6 +82,122 @@ function extractDexScreenerData(): TokenData | undefined {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract token mentions from Twitter/X
|
||||||
|
* Detects $TOKEN format (e.g., $BONK, $SOL)
|
||||||
|
*/
|
||||||
|
function extractTwitterTokens(): TokenData[] {
|
||||||
|
const tokens: TokenData[] = [];
|
||||||
|
const pageText = document.body.innerText;
|
||||||
|
|
||||||
|
// Match $TOKEN pattern (e.g., $BONK, $SOL, $PEPE)
|
||||||
|
const tokenPattern = /\$([A-Z]{2,10})\b/g;
|
||||||
|
const matches = pageText.matchAll(tokenPattern);
|
||||||
|
|
||||||
|
const uniqueTokens = new Set<string>();
|
||||||
|
for (const match of matches) {
|
||||||
|
const symbol = match[1];
|
||||||
|
if (!uniqueTokens.has(symbol)) {
|
||||||
|
uniqueTokens.add(symbol);
|
||||||
|
tokens.push({
|
||||||
|
chain: "solana", // Default to Solana, can be enhanced
|
||||||
|
pairAddress: "", // Will be resolved via API
|
||||||
|
tokenSymbol: symbol,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract contract addresses from page content
|
||||||
|
* Supports Solana and Ethereum address formats
|
||||||
|
*/
|
||||||
|
function extractContractAddresses(): TokenData[] {
|
||||||
|
const tokens: TokenData[] = [];
|
||||||
|
const pageText = document.body.innerText;
|
||||||
|
|
||||||
|
// Solana address pattern (base58, 32-44 characters)
|
||||||
|
const solanaPattern = /\b([1-9A-HJ-NP-Za-km-z]{32,44})\b/g;
|
||||||
|
|
||||||
|
// Ethereum address pattern (0x followed by 40 hex characters)
|
||||||
|
const ethPattern = /\b(0x[a-fA-F0-9]{40})\b/g;
|
||||||
|
|
||||||
|
// Extract Ethereum addresses
|
||||||
|
const ethMatches = pageText.matchAll(ethPattern);
|
||||||
|
for (const match of ethMatches) {
|
||||||
|
const address = match[1];
|
||||||
|
tokens.push({
|
||||||
|
chain: "ethereum",
|
||||||
|
pairAddress: address,
|
||||||
|
tokenSymbol: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract Solana addresses (more selective to avoid false positives)
|
||||||
|
const solanaMatches = pageText.matchAll(solanaPattern);
|
||||||
|
const uniqueSolanaAddresses = new Set<string>();
|
||||||
|
|
||||||
|
for (const match of solanaMatches) {
|
||||||
|
const address = match[1];
|
||||||
|
// Basic validation: should not be all same character, should have variety
|
||||||
|
if (address.length >= 32 &&
|
||||||
|
address.length <= 44 &&
|
||||||
|
new Set(address).size > 10 &&
|
||||||
|
!uniqueSolanaAddresses.has(address)) {
|
||||||
|
uniqueSolanaAddresses.add(address);
|
||||||
|
tokens.push({
|
||||||
|
chain: "solana",
|
||||||
|
pairAddress: address,
|
||||||
|
tokenSymbol: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens.slice(0, 5); // Limit to first 5 to avoid spam
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract trading pairs from page content
|
||||||
|
* Detects patterns like TOKEN/SOL, TOKEN/USDT, etc.
|
||||||
|
*/
|
||||||
|
function extractTradingPairs(): TokenData[] {
|
||||||
|
const tokens: TokenData[] = [];
|
||||||
|
const pageText = document.body.innerText;
|
||||||
|
|
||||||
|
// Match trading pair patterns (e.g., BONK/SOL, PEPE/USDT)
|
||||||
|
const pairPattern = /\b([A-Z]{2,10})\/([A-Z]{2,10})\b/g;
|
||||||
|
const matches = pageText.matchAll(pairPattern);
|
||||||
|
|
||||||
|
const uniquePairs = new Set<string>();
|
||||||
|
for (const match of matches) {
|
||||||
|
const baseToken = match[1];
|
||||||
|
const quoteToken = match[2];
|
||||||
|
const pairKey = `${baseToken}/${quoteToken}`;
|
||||||
|
|
||||||
|
if (!uniquePairs.has(pairKey)) {
|
||||||
|
uniquePairs.add(pairKey);
|
||||||
|
tokens.push({
|
||||||
|
chain: "solana", // Default to Solana
|
||||||
|
pairAddress: "", // Will be resolved via API
|
||||||
|
tokenSymbol: baseToken,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens.slice(0, 3); // Limit to first 3 pairs
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageContext {
|
||||||
|
url: string;
|
||||||
|
title: string;
|
||||||
|
pageType: PageType;
|
||||||
|
tokenData?: TokenData;
|
||||||
|
/** Detected tokens from page content (Twitter mentions, addresses, pairs) */
|
||||||
|
detectedTokens?: TokenData[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract page context based on page type
|
* Extract page context based on page type
|
||||||
*/
|
*/
|
||||||
|
|
@ -99,6 +215,37 @@ function extractPageContext(): PageContext {
|
||||||
// Add page-specific data
|
// Add page-specific data
|
||||||
if (pageType === "dexscreener") {
|
if (pageType === "dexscreener") {
|
||||||
context.tokenData = extractDexScreenerData();
|
context.tokenData = extractDexScreenerData();
|
||||||
|
} else if (pageType === "twitter") {
|
||||||
|
// Extract Twitter token mentions
|
||||||
|
const twitterTokens = extractTwitterTokens();
|
||||||
|
const contractAddresses = extractContractAddresses();
|
||||||
|
const tradingPairs = extractTradingPairs();
|
||||||
|
|
||||||
|
// Combine all detected tokens
|
||||||
|
context.detectedTokens = [
|
||||||
|
...twitterTokens,
|
||||||
|
...contractAddresses,
|
||||||
|
...tradingPairs,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Set primary token if available
|
||||||
|
if (context.detectedTokens.length > 0) {
|
||||||
|
context.tokenData = context.detectedTokens[0];
|
||||||
|
}
|
||||||
|
} else if (pageType === "generic") {
|
||||||
|
// For generic pages, try to detect contract addresses and trading pairs
|
||||||
|
const contractAddresses = extractContractAddresses();
|
||||||
|
const tradingPairs = extractTradingPairs();
|
||||||
|
|
||||||
|
context.detectedTokens = [
|
||||||
|
...contractAddresses,
|
||||||
|
...tradingPairs,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Set primary token if available
|
||||||
|
if (context.detectedTokens.length > 0) {
|
||||||
|
context.tokenData = context.detectedTokens[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ import {
|
||||||
import { SafetyScoreDisplay } from "../crypto/SafetyScoreDisplay";
|
import { SafetyScoreDisplay } from "../crypto/SafetyScoreDisplay";
|
||||||
import { WatchlistPanel } from "../crypto/WatchlistPanel";
|
import { WatchlistPanel } from "../crypto/WatchlistPanel";
|
||||||
import { AlertConfigModal } from "../crypto/AlertConfigModal";
|
import { AlertConfigModal } from "../crypto/AlertConfigModal";
|
||||||
|
import { DetectedTokensList } from "../components/DetectedTokensList";
|
||||||
import type { WatchlistItem } from "../widgets";
|
import type { WatchlistItem } from "../widgets";
|
||||||
|
import type { TokenData } from "../context/PageContextProvider";
|
||||||
|
|
||||||
type ViewMode = "chat" | "watchlist" | "safety";
|
type ViewMode = "chat" | "watchlist" | "safety";
|
||||||
|
|
||||||
|
|
@ -471,6 +473,14 @@ What would you like to know?`;
|
||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle detected token click
|
||||||
|
*/
|
||||||
|
const handleDetectedTokenClick = (token: TokenData) => {
|
||||||
|
const query = token.tokenSymbol || token.pairAddress;
|
||||||
|
handleTokenSearch(query);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Header with space selector and settings */}
|
{/* Header with space selector and settings */}
|
||||||
|
|
@ -495,6 +505,14 @@ What would you like to know?`;
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Detected tokens list (on Twitter and other pages) */}
|
||||||
|
{context?.detectedTokens && context.detectedTokens.length > 0 && viewMode === "chat" && (
|
||||||
|
<DetectedTokensList
|
||||||
|
tokens={context.detectedTokens}
|
||||||
|
onTokenClick={handleDetectedTokenClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Main content area */}
|
{/* Main content area */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
{viewMode === "chat" && (
|
{viewMode === "chat" && (
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { Coins, ExternalLink } from "lucide-react";
|
||||||
|
import { Button } from "@/routes/ui/button";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import type { TokenData } from "../context/PageContextProvider";
|
||||||
|
import { ChainIcon } from "./shared/ChainIcon";
|
||||||
|
|
||||||
|
export interface DetectedTokensListProps {
|
||||||
|
/** List of detected tokens */
|
||||||
|
tokens: TokenData[];
|
||||||
|
/** Callback when a token is clicked */
|
||||||
|
onTokenClick?: (token: TokenData) => void;
|
||||||
|
/** Additional class names */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DetectedTokensList - Display list of tokens detected from page content
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Shows tokens detected from Twitter mentions, contract addresses, trading pairs
|
||||||
|
* - Click to analyze token
|
||||||
|
* - Shows chain icon for each token
|
||||||
|
*/
|
||||||
|
export function DetectedTokensList({
|
||||||
|
tokens,
|
||||||
|
onTokenClick,
|
||||||
|
className,
|
||||||
|
}: DetectedTokensListProps) {
|
||||||
|
if (tokens.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("border-b bg-muted/30", className)}>
|
||||||
|
<div className="p-3">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Coins className="h-4 w-4 text-primary" />
|
||||||
|
<h3 className="text-sm font-semibold">Detected Tokens</h3>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
({tokens.length} found)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
{tokens.slice(0, 5).map((token, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => onTokenClick?.(token)}
|
||||||
|
className="w-full flex items-center justify-between p-2 rounded-md hover:bg-background transition-colors text-left"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
|
<ChainIcon chain={token.chain} size="sm" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
{token.tokenSymbol ? (
|
||||||
|
<div className="font-medium text-sm truncate">
|
||||||
|
{token.tokenSymbol}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-xs text-muted-foreground font-mono truncate">
|
||||||
|
{token.pairAddress.slice(0, 8)}...{token.pairAddress.slice(-6)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{token.tokenName && (
|
||||||
|
<div className="text-xs text-muted-foreground truncate">
|
||||||
|
{token.tokenName}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ExternalLink className="h-3 w-3 text-muted-foreground flex-shrink-0" />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tokens.length > 5 && (
|
||||||
|
<div className="text-xs text-muted-foreground text-center mt-2">
|
||||||
|
+{tokens.length - 5} more tokens detected
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -23,6 +23,8 @@ export interface PageContext {
|
||||||
title: string;
|
title: string;
|
||||||
pageType: PageType;
|
pageType: PageType;
|
||||||
tokenData?: TokenData;
|
tokenData?: TokenData;
|
||||||
|
/** Detected tokens from page content (Twitter mentions, addresses, pairs) */
|
||||||
|
detectedTokens?: TokenData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PageContextValue {
|
interface PageContextValue {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue