diff --git a/surfsense_browser_extension/sidepanel/capture/AnnotationTools.tsx b/surfsense_browser_extension/sidepanel/capture/AnnotationTools.tsx new file mode 100644 index 000000000..7e470140d --- /dev/null +++ b/surfsense_browser_extension/sidepanel/capture/AnnotationTools.tsx @@ -0,0 +1,171 @@ +import { useState } from "react"; +import { cn } from "~/lib/utils"; +import { + Minus, + ArrowRight, + Type, + Circle, + Square, + TrendingUp, + Eraser, + Undo, + Redo, +} from "lucide-react"; +import { Button } from "@/routes/ui/button"; + +export type AnnotationType = "line" | "arrow" | "text" | "circle" | "rectangle" | "fibonacci"; + +export interface Annotation { + id: string; + type: AnnotationType; + coordinates: { x: number; y: number }[]; + text?: string; + color: string; +} + +export interface AnnotationToolsProps { + /** Callback when annotation is added */ + onAnnotationAdd?: (annotation: Annotation) => void; + /** Callback when annotation is removed */ + onAnnotationRemove?: (id: string) => void; + /** Callback when undo is clicked */ + onUndo?: () => void; + /** Callback when redo is clicked */ + onRedo?: () => void; + /** Additional class names */ + className?: string; +} + +/** + * AnnotationTools - Drawing tools for chart annotations + * + * Features: + * - Line tool for trend lines, support/resistance + * - Arrow tool for directional indicators + * - Text tool for labels + * - Shape tools (circle, rectangle) + * - Fibonacci retracement tool + * - Color picker + * - Undo/Redo functionality + */ +export function AnnotationTools({ + onAnnotationAdd, + onAnnotationRemove, + onUndo, + onRedo, + className, +}: AnnotationToolsProps) { + const [selectedTool, setSelectedTool] = useState(null); + const [selectedColor, setSelectedColor] = useState("#3b82f6"); // blue-500 + + const tools: { type: AnnotationType; icon: any; label: string }[] = [ + { type: "line", icon: Minus, label: "Line" }, + { type: "arrow", icon: ArrowRight, label: "Arrow" }, + { type: "text", icon: Type, label: "Text" }, + { type: "circle", icon: Circle, label: "Circle" }, + { type: "rectangle", icon: Square, label: "Rectangle" }, + { type: "fibonacci", icon: TrendingUp, label: "Fibonacci" }, + ]; + + const colors = [ + { value: "#3b82f6", label: "Blue" }, + { value: "#ef4444", label: "Red" }, + { value: "#22c55e", label: "Green" }, + { value: "#eab308", label: "Yellow" }, + { value: "#a855f7", label: "Purple" }, + { value: "#ffffff", label: "White" }, + ]; + + const handleToolSelect = (tool: AnnotationType) => { + setSelectedTool(tool === selectedTool ? null : tool); + }; + + return ( +
+ {/* Drawing Tools */} +
+

Drawing Tools

+
+ {tools.map(({ type, icon: Icon, label }) => ( + + ))} +
+
+ + {/* Color Picker */} +
+

Color

+
+ {colors.map(({ value, label }) => ( +
+
+ + {/* Actions */} +
+ + + +
+ + {/* Instructions */} + {selectedTool && ( +
+ {selectedTool === "line" && "Click and drag to draw a line"} + {selectedTool === "arrow" && "Click and drag to draw an arrow"} + {selectedTool === "text" && "Click to add text label"} + {selectedTool === "circle" && "Click and drag to draw a circle"} + {selectedTool === "rectangle" && "Click and drag to draw a rectangle"} + {selectedTool === "fibonacci" && "Click two points to draw Fibonacci retracement"} +
+ )} +
+ ); +} + diff --git a/surfsense_browser_extension/sidepanel/capture/ChartCapturePanel.tsx b/surfsense_browser_extension/sidepanel/capture/ChartCapturePanel.tsx new file mode 100644 index 000000000..f23a39ef0 --- /dev/null +++ b/surfsense_browser_extension/sidepanel/capture/ChartCapturePanel.tsx @@ -0,0 +1,271 @@ +import { useState } from "react"; +import { cn } from "~/lib/utils"; +import { + Camera, + Download, + Copy, + Twitter, + MessageSquare, + Image as ImageIcon, + Palette, + Settings, +} from "lucide-react"; +import { Button } from "@/routes/ui/button"; +import { AnnotationTools } from "./AnnotationTools"; + +export interface ChartCaptureMetadata { + tokenSymbol: string; + tokenName: string; + price: number; + change24h: number; + volume: number; + liquidity: number; + timestamp: Date; +} + +export interface ChartCaptureSettings { + style: "dark" | "light" | "neon"; + includeTokenInfo: boolean; + includePriceChange: boolean; + includeVolumeLiquidity: boolean; + includeTimestamp: boolean; + includeWatermark: boolean; +} + +export interface ChartCapturePanelProps { + /** Current token metadata */ + metadata?: ChartCaptureMetadata; + /** Callback when capture is clicked */ + onCapture?: () => void; + /** Callback when export is clicked */ + onExport?: (format: "twitter" | "telegram" | "instagram" | "clipboard") => void; + /** Additional class names */ + className?: string; +} + +/** + * ChartCapturePanel - Chart screenshot tool with annotations + * + * Features: + * - One-click chart capture from DexScreener + * - Auto-add metadata overlay (token info, price, volume, etc.) + * - Drawing tools (lines, arrows, text, shapes, Fibonacci) + * - Template styles (dark, light, neon) + * - Export options (Twitter, Telegram, Instagram, clipboard) + */ +export function ChartCapturePanel({ + metadata, + onCapture, + onExport, + className, +}: ChartCapturePanelProps) { + const [settings, setSettings] = useState({ + style: "dark", + includeTokenInfo: true, + includePriceChange: true, + includeVolumeLiquidity: true, + includeTimestamp: true, + includeWatermark: false, + }); + + const [capturedImage, setCapturedImage] = useState(null); + const [isCapturing, setIsCapturing] = useState(false); + + const handleCapture = async () => { + setIsCapturing(true); + await onCapture?.(); + // Mock: simulate capture + setTimeout(() => { + setCapturedImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="); + setIsCapturing(false); + }, 1000); + }; + + const handleExport = (format: "twitter" | "telegram" | "instagram" | "clipboard") => { + onExport?.(format); + }; + + const formatCurrency = (value: number) => { + if (value >= 1000000) return `$${(value / 1000000).toFixed(2)}M`; + if (value >= 1000) return `$${(value / 1000).toFixed(1)}K`; + return `$${value.toFixed(2)}`; + }; + + return ( +
+ {/* Header */} +
+
+ +
+

Chart Capture

+ {metadata && ( +

+ {metadata.tokenSymbol} +

+ )} +
+
+
+ + {/* Content */} +
+ {/* Capture Button */} + {!capturedImage && ( + + )} + + {/* Preview */} + {capturedImage && ( +
+
+ Captured chart + {/* Metadata Overlay Preview */} + {metadata && settings.includeTokenInfo && ( +
+
{metadata.tokenSymbol}
+ {settings.includePriceChange && ( +
= 0 ? "text-green-600" : "text-red-600" + )}> + ${metadata.price.toFixed(6)} ({metadata.change24h >= 0 ? "+" : ""}{metadata.change24h.toFixed(2)}%) +
+ )} +
+ )} +
+ + {/* Annotation Tools */} + + + {/* Recapture Button */} + +
+ )} + + {/* Style Selection */} +
+
+ +

Style

+
+
+ {(["dark", "light", "neon"] as const).map((style) => ( + + ))} +
+
+ + {/* Metadata Options */} +
+
+ +

Metadata

+
+
+ {[ + { key: "includeTokenInfo" as const, label: "Token info" }, + { key: "includePriceChange" as const, label: "Price & change" }, + { key: "includeVolumeLiquidity" as const, label: "Volume & liquidity" }, + { key: "includeTimestamp" as const, label: "Timestamp" }, + { key: "includeWatermark" as const, label: "Watermark" }, + ].map(({ key, label }) => ( + + ))} +
+
+
+ + {/* Footer - Export Options */} + {capturedImage && ( +
+

Export

+
+ + + + +
+ +
+ )} +
+ ); +} + diff --git a/surfsense_browser_extension/sidepanel/content/ThreadGeneratorPanel.tsx b/surfsense_browser_extension/sidepanel/content/ThreadGeneratorPanel.tsx new file mode 100644 index 000000000..874e5507d --- /dev/null +++ b/surfsense_browser_extension/sidepanel/content/ThreadGeneratorPanel.tsx @@ -0,0 +1,382 @@ +import { useState } from "react"; +import { cn } from "~/lib/utils"; +import { + MessageSquare, + Sparkles, + Copy, + Twitter, + Edit2, + Trash2, + Plus, + RefreshCw, +} from "lucide-react"; +import { Button } from "@/routes/ui/button"; +import { ChainIcon } from "../components/shared/ChainIcon"; + +export interface Tweet { + number: number; + content: string; + type: "hook" | "analysis" | "implication" | "conclusion" | "disclaimer"; + includeChart?: boolean; +} + +export interface ThreadRequest { + tokenAddress: string; + tokenSymbol: string; + chain: string; + topic?: string; + length: number; + tone: "bullish" | "neutral" | "bearish"; +} + +export interface GeneratedThread { + tweets: Tweet[]; + metadata: { + tokenSymbol: string; + keyStats: Record; + }; +} + +export interface ThreadGeneratorPanelProps { + /** Current token info */ + tokenAddress?: string; + tokenSymbol?: string; + chain?: string; + /** Callback when thread is generated */ + onGenerate?: (request: ThreadRequest) => void; + /** Callback when thread is exported */ + onExport?: (format: "copy" | "twitter") => void; + /** Additional class names */ + className?: string; +} + +/** + * ThreadGeneratorPanel - AI-powered Twitter thread generator + * + * Features: + * - Auto-fill token info from current page + * - Customizable thread length (5-10 tweets) + * - Tone selection (bullish/neutral/bearish) + * - AI-generated thread structure (Hook → Analysis → Implications → Conclusion) + * - Edit individual tweets + * - Reorder tweets + * - Export options (copy all, tweet directly) + */ +export function ThreadGeneratorPanel({ + tokenAddress, + tokenSymbol, + chain, + onGenerate, + onExport, + className, +}: ThreadGeneratorPanelProps) { + const [request, setRequest] = useState({ + tokenAddress: tokenAddress || "", + tokenSymbol: tokenSymbol || "", + chain: chain || "solana", + topic: "", + length: 7, + tone: "bullish", + }); + + const [generatedThread, setGeneratedThread] = useState(null); + const [isGenerating, setIsGenerating] = useState(false); + const [editingTweet, setEditingTweet] = useState(null); + + // Mock generated thread + const mockThread: GeneratedThread = { + tweets: [ + { + number: 1, + content: `🧵 ${request.tokenSymbol} is showing massive volume spike (+200%) in the last 24h. Here's what you need to know šŸ‘‡`, + type: "hook", + }, + { + number: 2, + content: `Contract analysis:\nāœ… Verified on-chain\nāœ… Ownership renounced\nāœ… LP locked for 90 days\nāœ… No proxy contracts\n\nSolid fundamentals from a security perspective.`, + type: "analysis", + }, + { + number: 3, + content: `Holder distribution looks healthy:\n• 1,234 holders\n• Top 10 hold only 35%\n• No single whale dominance\n\nThis suggests organic growth and reduced rug pull risk.`, + type: "analysis", + }, + { + number: 4, + content: `Liquidity: $50K\nVolume/Liquidity ratio: 2.0x\n\nStrong trading activity relative to liquidity. This is a bullish signal for price discovery.`, + type: "analysis", + }, + { + number: 5, + content: `Social sentiment is turning positive:\n• 500 Twitter mentions (24h)\n• 1,200 Telegram messages\n• Growing community engagement\n\nMomentum is building.`, + type: "implication", + }, + { + number: 6, + content: `What this means:\n\nWe're seeing early signs of a potential breakout. Volume precedes price, and the fundamentals support sustained growth.`, + type: "implication", + }, + { + number: 7, + content: `TL;DR:\nāœ… Verified & safe contract\nāœ… Healthy holder distribution\nāœ… Strong volume growth\nāœ… Positive social sentiment\n\nDYOR, but this one's worth watching closely. šŸ‘€`, + type: "conclusion", + }, + ], + metadata: { + tokenSymbol: request.tokenSymbol, + keyStats: { + price: 0.0001234, + change24h: 15.5, + volume: 100000, + liquidity: 50000, + }, + }, + }; + + const handleGenerate = async () => { + setIsGenerating(true); + await onGenerate?.(request); + // Mock: simulate generation + setTimeout(() => { + setGeneratedThread(mockThread); + setIsGenerating(false); + }, 2000); + }; + + const handleEditTweet = (number: number, newContent: string) => { + if (!generatedThread) return; + const updatedTweets = generatedThread.tweets.map((tweet) => + tweet.number === number ? { ...tweet, content: newContent } : tweet + ); + setGeneratedThread({ ...generatedThread, tweets: updatedTweets }); + setEditingTweet(null); + }; + + const handleDeleteTweet = (number: number) => { + if (!generatedThread) return; + const updatedTweets = generatedThread.tweets + .filter((tweet) => tweet.number !== number) + .map((tweet, index) => ({ ...tweet, number: index + 1 })); + setGeneratedThread({ ...generatedThread, tweets: updatedTweets }); + }; + + const handleAddTweet = () => { + if (!generatedThread) return; + const newTweet: Tweet = { + number: generatedThread.tweets.length + 1, + content: "New tweet content...", + type: "analysis", + }; + setGeneratedThread({ + ...generatedThread, + tweets: [...generatedThread.tweets, newTweet], + }); + }; + + return ( +
+ {/* Header */} +
+
+ +
+

AI Thread Generator

+

+ Create Twitter threads with AI +

+
+
+
+ + {/* Content */} +
+ {!generatedThread ? ( + <> + {/* Input Form */} +
+ {/* Token Info */} +
+ +
+ {request.tokenSymbol || \"Not selected\"} + {request.chain && } +
+
+ + {/* Topic */} +
+ + setRequest({ ...request, topic: e.target.value })} + className=\"w-full p-2 text-sm border rounded\" + /> +
+ + {/* Length */} +
+ + +
+ + {/* Tone */} +
+ +
+ {([\"bullish\", \"neutral\", \"bearish\"] as const).map((tone) => ( + + ))} +
+
+
+ + {/* Generate Button */} + + + ) : ( + <> + {/* Generated Thread Preview */} +
+
+

Preview

+ +
+ + {/* Tweets */} +
+ {generatedThread.tweets.map((tweet) => ( +
+ {editingTweet === tweet.number ? ( +
+