mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-01 11:56:25 +02:00
feat: add new widgets for holder analysis, live token data, price, market overview, and trending tokens
- Implemented HolderAnalysisWidget to display holder distribution and concentration risk. - Created LiveTokenDataWidget for real-time market data including price changes and transaction activity. - Added LiveTokenPriceWidget to show current token price and changes over various timeframes. - Developed MarketOverviewWidget to provide a summary of market statistics and token prices. - Introduced TrendingTokensWidget to showcase trending tokens with price changes and volume. - Added TradingSuggestionToolUI for AI-powered trading suggestions with detailed entry, targets, and stop-loss information. - Enhanced settings components for better user configuration options in the SurfSense Browser Extension.
This commit is contained in:
parent
2bf40ab5ce
commit
8bc092e40e
23 changed files with 2173 additions and 111 deletions
|
|
@ -1,8 +1,18 @@
|
|||
# SurfSense 2.0 - Epics & Stories
|
||||
|
||||
**Project:** SurfSense Crypto Co-Pilot
|
||||
**Created:** 2026-02-01
|
||||
**Status:** Planning Complete
|
||||
**Project:** SurfSense Crypto Co-Pilot
|
||||
**Created:** 2026-02-01
|
||||
**Updated:** 2026-02-04
|
||||
**Status:** 🚧 IN DEVELOPMENT
|
||||
|
||||
---
|
||||
|
||||
## Strategy Update (2026-02-04)
|
||||
|
||||
> **Extension = Full Features** (không chỉ Quick Actions)
|
||||
>
|
||||
> Extension là **full-featured crypto co-pilot** với đầy đủ tính năng phân tích, monitoring, và trading intelligence.
|
||||
> Web Dashboard tập trung vào settings management và detailed analytics.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -14,98 +24,109 @@ Tài liệu này tổ chức tất cả epics và user stories cho SurfSense 2.0
|
|||
|
||||
## Epic Summary
|
||||
|
||||
| Epic | Phase | Stories | Status | Priority | Duration |
|
||||
|------|-------|---------|--------|----------|----------|
|
||||
| [Epic 1: Extension Core Infrastructure](./epic-1-extension-core-infrastructure.md) | Phase 1 | 6 | ✅ COMPLETED | P0 | 2 weeks |
|
||||
| [Epic 2: Smart Monitoring & Alerts](./epic-2-smart-monitoring-alerts.md) | Phase 2 | 3 | 📋 PLANNED | P0 | 2 weeks |
|
||||
| [Epic 3: Trading Intelligence](./epic-3-trading-intelligence.md) | Phase 3 | 3 | 📋 PLANNED | P1 | 2 weeks |
|
||||
| [Epic 4: Content Creation & Productivity](./epic-4-content-creation-productivity.md) | Phase 4 | 3 | 📋 PLANNED | P2 | 2 weeks |
|
||||
| Epic | Phase | Stories | Status | Frontend | Backend | Priority |
|
||||
|------|-------|---------|--------|----------|---------|----------|
|
||||
| [Epic 1: AI-Powered Crypto Assistant](./epic-1-ai-powered-crypto-assistant.md) | Phase 1 | 10 | 🚧 IN PROGRESS | 100% ✅ | 0% ❌ | P0 |
|
||||
| [Epic 2: Smart Monitoring & Alerts](./epic-2-smart-monitoring-alerts.md) | Phase 2 | 3 | 🚧 IN PROGRESS | 100% ✅ | 0% ❌ | P0 |
|
||||
| [Epic 3: Trading Intelligence](./epic-3-trading-intelligence.md) | Phase 3 | 3 | 🚧 IN PROGRESS | 100% ✅ | 0% ❌ | P1 |
|
||||
| [Epic 4: Content Creation & Productivity](./epic-4-content-creation-productivity.md) | Phase 4 | 5 | 🚧 IN PROGRESS | 100% ✅ | 0% ❌ | P2 |
|
||||
|
||||
**Total:** 4 epics, 15 user stories, 8 weeks
|
||||
**Total:** 4 epics, 21 user stories
|
||||
|
||||
**🔴 BLOCKER:** Backend APIs chưa được implement (Authentication, Settings, Chat sync, Data APIs)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Extension Core Infrastructure ✅
|
||||
## Phase 1: AI-Powered Crypto Assistant 🚧
|
||||
|
||||
**Epic 1** - Foundation cho tất cả features
|
||||
|
||||
### Stories:
|
||||
1. **Story 1.1:** Side Panel Architecture (FR-EXT-01)
|
||||
2. **Story 1.2:** AI Chat Interface Integration (FR-EXT-02)
|
||||
3. **Story 1.3:** Page Context Detection (FR-EXT-03)
|
||||
4. **Story 1.4:** DexScreener Smart Integration (FR-EXT-04)
|
||||
5. **Story 1.5:** Quick Capture (FR-EXT-05)
|
||||
6. **Story 1.6:** Settings Sync với Frontend (FR-EXT-06)
|
||||
1. **Story 1.0:** Authentication System (FR-EXT-00) - ⏳ BACKEND BLOCKER
|
||||
2. **Story 1.1:** Side Panel Architecture (FR-EXT-01) - ✅ DONE
|
||||
3. **Story 1.2:** AI Chat Interface Integration (FR-EXT-02) - ✅ DONE
|
||||
4. **Story 1.3:** Page Context Detection (FR-EXT-03) - ✅ DONE
|
||||
5. **Story 1.4:** DexScreener Smart Integration (FR-EXT-04) - ✅ DONE
|
||||
6. **Story 1.5:** Quick Capture (FR-EXT-05) - ✅ DONE
|
||||
7. **Story 1.6:** Settings Sync với Frontend (FR-EXT-06) - ⏳ BACKEND PENDING
|
||||
8. **Story 1.7:** Universal Token Search Bar (FR-EXT-07) - ✅ DONE (NEW)
|
||||
9. **Story 1.8:** Multi-Page Token Detection (FR-EXT-08) - ✅ DONE (NEW)
|
||||
10. **Story 1.9:** Floating Quick Action Button (FR-EXT-09) - ✅ DONE (NEW)
|
||||
|
||||
**Key Deliverables:**
|
||||
- ✅ Chrome Side Panel working
|
||||
- ✅ AI chat interface integrated
|
||||
- ✅ Context detection for DexScreener
|
||||
- ✅ Context detection for DexScreener, Twitter, etc.
|
||||
- ✅ Token info card
|
||||
- ✅ Quick capture button
|
||||
- ✅ Settings sync infrastructure
|
||||
- ✅ Universal token search
|
||||
- ✅ Multi-page token detection
|
||||
- ✅ Floating quick action button
|
||||
- ⏳ Settings sync (needs backend)
|
||||
- ❌ Authentication (needs backend)
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Smart Monitoring & Alerts 📋
|
||||
## Phase 2: Smart Monitoring & Alerts 🚧
|
||||
|
||||
**Epic 2** - Risk protection & opportunity alerts
|
||||
|
||||
### Stories:
|
||||
1. **Story 2.1:** Real-time Price Alerts (FR-EXT-07)
|
||||
2. **Story 2.2:** Whale Activity Tracker (FR-EXT-08)
|
||||
3. **Story 2.3:** Rug Pull Early Warning System (FR-EXT-09)
|
||||
1. **Story 2.1:** Real-time Price Alerts (FR-EXT-10) - ✅ UI DONE, ⏳ API PENDING
|
||||
2. **Story 2.2:** Whale Activity Tracker (FR-EXT-11) - ✅ UI DONE, ⏳ API PENDING
|
||||
3. **Story 2.3:** Rug Pull Early Warning System (FR-EXT-12) - ✅ UI DONE, ⏳ API PENDING
|
||||
|
||||
**Key Deliverables:**
|
||||
- Watchlist management
|
||||
- Price/volume/liquidity alerts
|
||||
- Browser notifications
|
||||
- Whale transaction monitoring
|
||||
- Rug pull risk assessment
|
||||
- Risk score display
|
||||
- ✅ Watchlist management UI (WatchlistPanel, WatchlistWidget)
|
||||
- ✅ Alert configuration UI (AlertConfigModal, AlertWidget)
|
||||
- ✅ Whale activity UI (WhaleActivityFeed, WhaleActivityWidget)
|
||||
- ✅ Safety score display (SafetyScoreDisplay)
|
||||
- ⏳ Browser notifications (needs backend)
|
||||
- ⏳ Real-time data (needs DexScreener API)
|
||||
|
||||
**Business Value:** Risk protection = User trust
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Trading Intelligence 📋
|
||||
## Phase 3: Trading Intelligence 🚧
|
||||
|
||||
**Epic 3** - AI-powered trading insights
|
||||
|
||||
### Stories:
|
||||
1. **Story 3.1:** One-Click Token Analysis (FR-EXT-10)
|
||||
2. **Story 3.2:** Smart Entry/Exit Suggestions (FR-EXT-11)
|
||||
3. **Story 3.3:** Portfolio Tracker Integration (FR-EXT-12)
|
||||
1. **Story 3.1:** One-Click Token Analysis (FR-EXT-13) - ✅ UI DONE, ⏳ API PENDING
|
||||
2. **Story 3.2:** Smart Entry/Exit Suggestions (FR-EXT-14) - ✅ UI DONE, ⏳ API PENDING
|
||||
3. **Story 3.3:** Portfolio Tracker Integration (FR-EXT-15) - ✅ UI DONE, ⏳ API PENDING
|
||||
|
||||
**Key Deliverables:**
|
||||
- Comprehensive token analysis
|
||||
- AI-generated summaries
|
||||
- Entry/exit suggestions
|
||||
- Risk/reward calculations
|
||||
- Wallet connection
|
||||
- Portfolio tracking
|
||||
- P&L analytics
|
||||
- ✅ Token analysis UI (TokenAnalysisPanel, TokenAnalysisWidget)
|
||||
- ✅ Trading suggestions UI (TradingSuggestionPanel, TradingSuggestionWidget)
|
||||
- ✅ Portfolio UI (PortfolioPanel, PortfolioWidget)
|
||||
- ⏳ AI-generated summaries (needs backend)
|
||||
- ⏳ Wallet connection (needs integration)
|
||||
- ⏳ Real-time P&L (needs backend)
|
||||
|
||||
**Business Value:** Better decisions = Better results = Happy users
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Content Creation & Productivity 📋
|
||||
## Phase 4: Content Creation & Productivity ✅
|
||||
|
||||
**Epic 4** - Tools for creators & power users
|
||||
|
||||
### Stories:
|
||||
1. **Story 4.1:** Chart Screenshot with Annotations (FR-EXT-13)
|
||||
2. **Story 4.2:** AI Thread Generator (FR-EXT-14)
|
||||
3. **Story 4.3:** Quick Actions & Productivity (FR-EXT-15, 16, 17)
|
||||
1. **Story 4.1:** Chart Screenshot with Annotations (FR-EXT-16) - ✅ UI DONE
|
||||
2. **Story 4.2:** AI Thread Generator (FR-EXT-17) - ✅ UI DONE, ⏳ AI PENDING
|
||||
3. **Story 4.3:** Quick Actions Context Menu (FR-EXT-18) - ✅ UI DONE
|
||||
4. **Story 4.4:** Smart Notifications Management (FR-EXT-19) - ✅ UI DONE
|
||||
5. **Story 4.5:** Keyboard Shortcuts (FR-EXT-20) - ✅ UI DONE
|
||||
|
||||
**Key Deliverables:**
|
||||
- Chart capture & annotation
|
||||
- Drawing tools
|
||||
- AI thread generation
|
||||
- Context menu quick actions
|
||||
- Smart notifications
|
||||
- Keyboard shortcuts
|
||||
- ✅ Chart capture UI (ChartCapturePanel, ChartCaptureWidget)
|
||||
- ✅ Thread generator UI (ThreadGeneratorPanel, ThreadGeneratorWidget)
|
||||
- ✅ Context menu quick actions (background/index.ts, useContextAction hook)
|
||||
- ✅ Smart notifications UI (NotificationSettingsPanel, NotificationsList)
|
||||
- ✅ Keyboard shortcuts (4 shortcuts: Analyze, Watchlist, Capture, Portfolio)
|
||||
- ⏳ AI thread generation (needs backend)
|
||||
|
||||
**Business Value:** Content creation = Viral marketing
|
||||
|
||||
|
|
@ -113,49 +134,75 @@ Tài liệu này tổ chức tất cả epics và user stories cho SurfSense 2.0
|
|||
|
||||
## Implementation Roadmap
|
||||
|
||||
### Week 1-2: Phase 1 ✅
|
||||
### Week 1-2: Phase 1 - Core Infrastructure
|
||||
- [x] Side panel architecture
|
||||
- [x] Chat interface
|
||||
- [x] Context detection
|
||||
- [x] Context detection (DexScreener, Twitter, etc.)
|
||||
- [x] DexScreener integration
|
||||
- [x] Quick capture
|
||||
- [x] Settings sync
|
||||
- [x] Universal token search (NEW)
|
||||
- [x] Multi-page token detection (NEW)
|
||||
- [x] Floating quick action button (NEW)
|
||||
- [ ] Authentication (BLOCKER)
|
||||
- [ ] Settings sync API
|
||||
|
||||
### Week 3-4: Phase 2 (Next)
|
||||
- [ ] Watchlist & alerts
|
||||
- [ ] Whale tracker
|
||||
- [ ] Rug pull detection
|
||||
### Week 3-4: Phase 2 - Smart Monitoring
|
||||
- [x] Watchlist UI (WatchlistPanel, WatchlistWidget)
|
||||
- [x] Alert config UI (AlertConfigModal, AlertWidget)
|
||||
- [x] Whale activity UI (WhaleActivityFeed, WhaleActivityWidget)
|
||||
- [x] Safety score UI (SafetyScoreDisplay)
|
||||
- [ ] DexScreener API integration
|
||||
- [ ] Real-time price alerts
|
||||
- [ ] Browser notifications
|
||||
|
||||
### Week 5-6: Phase 3
|
||||
- [ ] Token analysis
|
||||
- [ ] Trading suggestions
|
||||
- [ ] Portfolio tracker
|
||||
### Week 5-6: Phase 3 - Trading Intelligence
|
||||
- [x] Token analysis UI (TokenAnalysisPanel, TokenAnalysisWidget)
|
||||
- [x] Trading suggestions UI (TradingSuggestionPanel, TradingSuggestionWidget)
|
||||
- [x] Portfolio UI (PortfolioPanel, PortfolioWidget)
|
||||
- [ ] AI analysis backend
|
||||
- [ ] Wallet connection
|
||||
- [ ] Real-time P&L
|
||||
|
||||
### Week 7-8: Phase 4
|
||||
- [ ] Chart capture
|
||||
- [ ] Thread generator
|
||||
- [ ] Productivity features
|
||||
### Week 7-8: Phase 4 - Content & Productivity
|
||||
- [x] Chart capture UI (ChartCapturePanel, ChartCaptureWidget)
|
||||
- [x] Thread generator UI (ThreadGeneratorPanel, ThreadGeneratorWidget)
|
||||
- [x] Context menu quick actions
|
||||
- [x] Smart notifications UI
|
||||
- [x] Keyboard shortcuts (4 shortcuts)
|
||||
- [ ] AI thread generation backend
|
||||
|
||||
---
|
||||
|
||||
## Feature Responsibility Matrix
|
||||
|
||||
| Feature | Extension | Frontend | Sync |
|
||||
|---------|-----------|----------|------|
|
||||
> **Strategy: Extension = Full Features**
|
||||
|
||||
| Feature | Extension | Web Dashboard | Sync |
|
||||
|---------|-----------|---------------|------|
|
||||
| Model Selection | 📖 Read-only | ✏️ Full control | API |
|
||||
| Search Space | 📖 Read-only | ✏️ Full control | API |
|
||||
| Chat | ✅ Full UI | ✅ Full UI | API |
|
||||
| Connectors | 📖 Use only | ✏️ Setup | API |
|
||||
| Documents | 👁️ View | ✏️ Manage | API |
|
||||
| Watchlist | ✏️ Add/Remove | ✏️ Full | Storage + API |
|
||||
| Alerts | ✏️ Basic | ✏️ Full | API |
|
||||
| Watchlist | ✅ Full | ✅ Full | API |
|
||||
| Alerts | ✅ Full | ✅ Full | API |
|
||||
| Token Analysis | ✅ Full | ✅ Full (via chat) | API |
|
||||
| Whale Activity | ✅ Full | ✅ Full (via chat) | API |
|
||||
| Trading Suggestions | ✅ Full | ✅ Full (via chat) | API |
|
||||
| Portfolio | ✅ Full | ✅ Full | API |
|
||||
| Chart Capture | ✅ Full | ❌ N/A | Local |
|
||||
| Thread Generator | ✅ Full | ❌ N/A | Local |
|
||||
| Context Detection | ✅ Full | ❌ N/A | Local |
|
||||
| Floating Button | ✅ Full | ❌ N/A | Local |
|
||||
| Settings | 📖 Quick | ✏️ Full | API |
|
||||
| Analytics | 👁️ Basic | ✅ Full | API |
|
||||
|
||||
**Legend:**
|
||||
- ✅ Full feature
|
||||
- ✏️ Full control
|
||||
- 📖 Read-only
|
||||
- 👁️ View only
|
||||
- ❌ N/A
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -163,17 +210,22 @@ Tài liệu này tổ chức tất cả epics và user stories cho SurfSense 2.0
|
|||
|
||||
### Frontend (Extension)
|
||||
- **Framework:** Plasmo (React + TypeScript)
|
||||
- **UI:** @assistant-ui/react, shadcn/ui
|
||||
- **UI:** @assistant-ui/react, shadcn/ui, Lucide icons
|
||||
- **State:** Plasmo Storage, React Context
|
||||
- **APIs:** Chrome Extension APIs
|
||||
- **APIs:** Chrome Extension APIs (sidePanel, storage, tabs, identity)
|
||||
|
||||
### Backend
|
||||
### Frontend (Web)
|
||||
- **Framework:** Next.js (React + TypeScript)
|
||||
- **UI:** shadcn/ui, @assistant-ui/react
|
||||
- **State:** React Context, Server Components
|
||||
|
||||
### Backend (⏳ PENDING)
|
||||
- **Framework:** FastAPI (Python)
|
||||
- **AI:** Gemini 1.5 Flash / GPT-4o-mini
|
||||
- **RAG:** Supabase (pgvector)
|
||||
- **Cache:** Redis
|
||||
|
||||
### Data Sources
|
||||
### Data Sources (⏳ PENDING)
|
||||
- DexScreener API
|
||||
- DefiLlama API
|
||||
- Helius (Solana)
|
||||
|
|
@ -183,43 +235,79 @@ Tài liệu này tổ chức tất cả epics và user stories cho SurfSense 2.0
|
|||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
## Current Progress
|
||||
|
||||
### Phase 1 (✅ COMPLETED)
|
||||
### Phase 1 (✅ 100% Frontend)
|
||||
- [x] Extension installable
|
||||
- [x] Chat works
|
||||
- [x] Chat works (mock data)
|
||||
- [x] Context detection works
|
||||
- [x] Token card displays
|
||||
- [x] Universal search works
|
||||
- [x] Multi-page detection works
|
||||
- [x] Floating button works
|
||||
- [ ] Authentication (BACKEND)
|
||||
- [ ] Real data from APIs (BACKEND)
|
||||
|
||||
### Phase 2 (Target)
|
||||
- [ ] 100+ tokens in watchlists
|
||||
- [ ] 1000+ alerts set
|
||||
- [ ] 0 false positive rug pull warnings
|
||||
### Phase 2 (✅ 100% UI)
|
||||
- [x] Watchlist UI complete
|
||||
- [x] Alert config UI complete
|
||||
- [x] Whale activity UI complete
|
||||
- [x] Safety score UI complete
|
||||
- [ ] Real-time data (BACKEND)
|
||||
- [ ] Browser notifications (BACKEND)
|
||||
|
||||
### Phase 3 (Target)
|
||||
- [ ] 500+ token analyses
|
||||
- [ ] 80%+ suggestion accuracy
|
||||
- [ ] 50+ wallets connected
|
||||
### Phase 3 (✅ 100% UI)
|
||||
- [x] Token analysis UI complete
|
||||
- [x] Trading suggestions UI complete
|
||||
- [x] Portfolio UI complete
|
||||
- [x] Holder analysis widget
|
||||
- [x] Market overview widget
|
||||
- [x] Trending tokens widget
|
||||
- [x] Live token price/data widgets
|
||||
- [ ] AI analysis (BACKEND)
|
||||
- [ ] Wallet connection (BACKEND)
|
||||
|
||||
### Phase 4 (Target)
|
||||
- [ ] 100+ charts captured
|
||||
- [ ] 50+ threads generated
|
||||
- [ ] 200+ daily shortcut uses
|
||||
### Phase 4 (✅ 100% UI)
|
||||
- [x] Chart capture UI complete
|
||||
- [x] Thread generator UI complete
|
||||
- [x] Context menu quick actions
|
||||
- [x] Smart notifications UI
|
||||
- [x] Keyboard shortcuts (4 shortcuts)
|
||||
- [ ] AI thread generation (BACKEND)
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Critical Blockers
|
||||
|
||||
1. **Backend Authentication (Story 1.0)**
|
||||
- Cần JWT + OAuth
|
||||
- Blocks: Settings sync, Chat sync, User data
|
||||
|
||||
2. **DexScreener API Integration**
|
||||
- Cần real-time token data
|
||||
- Blocks: All token-related features
|
||||
|
||||
3. **Backend APIs**
|
||||
- Settings, Chat, Capture endpoints
|
||||
- Blocks: Extension ↔ Web sync
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ **Epic 1 Complete** - Phase 1 infrastructure done
|
||||
2. 🎯 **Start Epic 2** - Begin Phase 2 implementation
|
||||
3. 📝 **Create Stories** - Break down Epic 2 into individual story files
|
||||
4. 🚀 **Sprint Planning** - Plan 2-week sprint for Phase 2
|
||||
1. 🔴 **PRIORITY: Backend Authentication** - Implement Story 1.0
|
||||
2. 🔴 **PRIORITY: DexScreener API** - Replace mock data with real data
|
||||
3. 🔴 **PRIORITY: Backend APIs** - Settings, Chat, Capture endpoints
|
||||
4. 🟡 **AI Thread Generation** - Backend for Epic 4.2
|
||||
5. 🟡 **Browser Notifications** - Real-time alerts
|
||||
6. 🟢 **Wallet Connection** - Portfolio integration
|
||||
|
||||
---
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [PRD](../_bmad-output/planning-artifacts/prd.md)
|
||||
- [Extension UX Strategy](../.gemini/antigravity/brain/02a071c7-57fc-4f43-a2e8-516ac511579a/extension-ux-integration-strategy.md)
|
||||
- [Extension Technical Spec](../.gemini/antigravity/brain/02a071c7-57fc-4f43-a2e8-516ac511579a/extension-sidepanel-technical-spec.md)
|
||||
- [Feature Brainstorming](../.gemini/antigravity/brain/02a071c7-57fc-4f43-a2e8-516ac511579a/extension-features-brainstorming.md)
|
||||
- [PRD v2](../_bmad-output/planning-artifacts/prd.md)
|
||||
- [Epic 1: AI-Powered Crypto Assistant](./epic-1-ai-powered-crypto-assistant.md)
|
||||
- [Epic 2: Smart Monitoring & Alerts](./epic-2-smart-monitoring-alerts.md)
|
||||
- [Epic 3: Trading Intelligence](./epic-3-trading-intelligence.md)
|
||||
- [Epic 4: Content Creation & Productivity](./epic-4-content-creation-productivity.md)
|
||||
|
|
|
|||
|
|
@ -1220,3 +1220,67 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||
- `lib/services/dexscreener-api.ts` (new)
|
||||
- `sidepanel/chat/ChatInterface.tsx`
|
||||
- `contents/floating-button.tsx`
|
||||
|
||||
---
|
||||
|
||||
## 📋 Backlog: Extension vs Web Sync
|
||||
|
||||
### Extension Widgets Còn Thiếu (cần thêm để sync với Web)
|
||||
|
||||
| Widget | Web Component | Priority | Mô tả |
|
||||
|--------|---------------|----------|-------|
|
||||
| `TrendingTokensWidget` | `TrendingTokensToolUI` | P1 | Hiển thị tokens đang trending |
|
||||
| `HolderAnalysisWidget` | `HolderAnalysisToolUI` | P1 | Phân tích holder distribution |
|
||||
| `MarketOverviewWidget` | `MarketOverviewToolUI` | P2 | Tổng quan thị trường (BTC, ETH, SOL) |
|
||||
| `LiveTokenPriceWidget` | `LiveTokenPriceToolUI` | P1 | Giá token real-time |
|
||||
| `LiveTokenDataWidget` | `LiveTokenDataToolUI` | P1 | Dữ liệu token chi tiết real-time |
|
||||
| `UserProfileWidget` | `UserProfileToolUI` | P2 | Hiển thị profile đầu tư của user |
|
||||
|
||||
**Files cần tạo:**
|
||||
- `sidepanel/widgets/TrendingTokensWidget.tsx` (new)
|
||||
- `sidepanel/widgets/HolderAnalysisWidget.tsx` (new)
|
||||
- `sidepanel/widgets/MarketOverviewWidget.tsx` (new)
|
||||
- `sidepanel/widgets/LiveTokenPriceWidget.tsx` (new)
|
||||
- `sidepanel/widgets/LiveTokenDataWidget.tsx` (new)
|
||||
- `sidepanel/widgets/UserProfileWidget.tsx` (new)
|
||||
|
||||
### Web Tool UIs Còn Thiếu (cần thêm để sync với Extension)
|
||||
|
||||
| Tool UI | Extension Component | Priority | Mô tả |
|
||||
|---------|---------------------|----------|-------|
|
||||
| `TradingSuggestionToolUI` | `TradingSuggestionWidget` | P1 | Gợi ý entry/exit points |
|
||||
|
||||
**Files cần tạo:**
|
||||
- `surfsense_web/components/tool-ui/crypto/trading-suggestion.tsx` (new)
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Critical Blockers
|
||||
|
||||
### 1. Backend Authentication (Story 1.0)
|
||||
- **Status:** ❌ Chưa bắt đầu
|
||||
- **Impact:** Blocks tất cả sync features
|
||||
- **Required:**
|
||||
- JWT token management
|
||||
- OAuth (Google) integration
|
||||
- Chrome Identity API wrapper
|
||||
- User session management
|
||||
|
||||
### 2. DexScreener API Integration
|
||||
- **Status:** ❌ Chưa bắt đầu
|
||||
- **Impact:** Tất cả token data đang là mock
|
||||
- **Required:**
|
||||
- API service với rate limiting
|
||||
- Caching layer (30s TTL)
|
||||
- Error handling với retry logic
|
||||
- Real-time price updates
|
||||
|
||||
### 3. Backend APIs
|
||||
- **Status:** ❌ Chưa bắt đầu
|
||||
- **Impact:** Extension không thể sync với Web
|
||||
- **Required:**
|
||||
- `/api/settings` - Settings sync
|
||||
- `/api/chat/messages` - Chat history sync
|
||||
- `/api/capture` - Page capture
|
||||
- `/api/watchlist` - Watchlist sync
|
||||
- `/api/alerts` - Alerts sync
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
# Epic 2: Smart Monitoring & Alerts (Giám sát & Cảnh báo Thông minh)
|
||||
|
||||
**Trạng thái:** 📋 ĐÃ LÊN KẾ HOẠCH (PLANNED)
|
||||
**Giai đoạn:** Phase 2
|
||||
**Thời gian:** 2 tuần
|
||||
**Trạng thái:** 🚧 ĐANG TRIỂN KHAI (IN PROGRESS)
|
||||
**Giai đoạn:** Phase 2
|
||||
**Thời gian:** 2 tuần
|
||||
**Mức độ ưu tiên:** P0 (Nghiêm trọng - Risk Protection)
|
||||
|
||||
**Tiến độ:**
|
||||
- ✅ Extension UI: 90% hoàn thành (WatchlistPanel, AlertConfigModal, WhaleActivityFeed, SafetyScoreDisplay)
|
||||
- ✅ Extension Widgets: 100% hoàn thành (WatchlistWidget, AlertWidget, WhaleActivityWidget)
|
||||
- ✅ Web Tool UIs: 100% hoàn thành (WatchlistDisplayToolUI, AlertConfigurationToolUI, WhaleActivityToolUI)
|
||||
- ⏳ Backend APIs: 0% (DexScreener integration, real-time data chưa implement)
|
||||
- ⏳ Browser Notifications: 0% (cần backend)
|
||||
|
||||
---
|
||||
|
||||
## Tổng quan Epic
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
# Epic 3: Trading Intelligence (Trí tuệ Giao dịch)
|
||||
|
||||
**Trạng thái:** 📋 ĐÃ LÊN KẾ HOẠCH (PLANNED)
|
||||
**Giai đoạn:** Phase 3
|
||||
**Thời gian:** 2 tuần
|
||||
**Trạng thái:** 🚧 ĐANG TRIỂN KHAI (IN PROGRESS)
|
||||
**Giai đoạn:** Phase 3
|
||||
**Thời gian:** 2 tuần
|
||||
**Mức độ ưu tiên:** P1 (Cao - Giá trị gia tăng)
|
||||
|
||||
**Tiến độ:**
|
||||
- ✅ Extension UI: 80% hoàn thành (TokenAnalysisPanel, TradingSuggestionPanel, PortfolioPanel)
|
||||
- ✅ Extension Widgets: 100% hoàn thành (TokenAnalysisWidget, TradingSuggestionWidget, PortfolioWidget)
|
||||
- ✅ Web Tool UIs: 80% hoàn thành (TokenAnalysisToolUI, PortfolioDisplayToolUI)
|
||||
- ⚠️ Web Tool UIs: TradingSuggestionToolUI chưa có (cần thêm để sync với Extension)
|
||||
- ⏳ Backend APIs: 0% (AI analysis, wallet connection chưa implement)
|
||||
- ⏳ Real-time P&L: 0% (cần backend)
|
||||
|
||||
---
|
||||
|
||||
## Tổng quan Epic
|
||||
|
|
|
|||
|
|
@ -1,10 +1,22 @@
|
|||
# Epic 4: Content Creation & Productivity (Tạo Nội dung & Hiệu suất)
|
||||
|
||||
**Trạng thái:** 📋 ĐÃ LÊN KẾ HOẠCH (PLANNED)
|
||||
**Giai đoạn:** Phase 4
|
||||
**Thời gian:** 2 tuần
|
||||
**Trạng thái:** 🚧 ĐANG TRIỂN KHAI (IN PROGRESS)
|
||||
**Giai đoạn:** Phase 4
|
||||
**Thời gian:** 2 tuần
|
||||
**Mức độ ưu tiên:** P2 (Trung bình - Nên có (Nice to Have))
|
||||
|
||||
**Tiến độ:**
|
||||
- ✅ Story 4.1 (Chart Capture): UI hoàn thành (ChartCapturePanel, ChartCaptureWidget, AnnotationTools)
|
||||
- ✅ Story 4.2 (Thread Generator): UI hoàn thành (ThreadGeneratorPanel, ThreadGeneratorWidget)
|
||||
- ✅ Story 4.3 (Quick Actions): UI hoàn thành (Context menu in background/index.ts, useContextAction hook)
|
||||
- ✅ Story 4.4 (Smart Notifications): UI hoàn thành (NotificationSettingsPanel, NotificationsList)
|
||||
- ✅ Story 4.5 (Keyboard Shortcuts): UI hoàn thành (4 shortcuts: Analyze, Watchlist, Capture, Portfolio)
|
||||
- ⏳ Backend APIs: 0% (AI thread generation chưa implement)
|
||||
|
||||
**Frontend UI: 100% ✅**
|
||||
|
||||
**Lưu ý:** Đây là Extension-only features (không có trên Web Dashboard)
|
||||
|
||||
---
|
||||
|
||||
## Tổng quan Epic
|
||||
|
|
|
|||
|
|
@ -290,6 +290,11 @@ Traders hiện đang đối mặt với **Quá tải Thông tin** và **Quy trì
|
|||
|
||||
##### Feature Responsibility Matrix
|
||||
|
||||
> **Strategy Update (2026-02-04):** Extension = Full Features
|
||||
>
|
||||
> Extension không chỉ là "Quick Actions" mà là **full-featured crypto co-pilot** với đầy đủ tính năng.
|
||||
> Web Dashboard là nơi quản lý settings và xem analytics chi tiết.
|
||||
|
||||
| Feature | Extension | Frontend Dashboard | Sync Method |
|
||||
|---------|-----------|-------------------|-------------|
|
||||
| **Model Selection** | 📖 Read-only dropdown | ✏️ Full selector | Backend API |
|
||||
|
|
@ -297,15 +302,37 @@ Traders hiện đang đối mặt với **Quá tải Thông tin** và **Quy trì
|
|||
| **Chat** | ✅ Full chat UI | ✅ Full chat UI | Backend API |
|
||||
| **Connectors** | 📖 Use only | ✏️ Setup & manage | Backend API |
|
||||
| **Documents** | 👁️ View in chat | ✏️ Full management | Backend API |
|
||||
| **Watchlist** | ✏️ Add/Remove | ✏️ Full management | Plasmo Storage + API |
|
||||
| **Alerts** | ✏️ Configure basic | ✏️ Full management | Backend API |
|
||||
| **Watchlist** | ✅ Full management | ✅ Full management | Backend API |
|
||||
| **Alerts** | ✅ Full management | ✅ Full management | Backend API |
|
||||
| **Token Analysis** | ✅ Full analysis | ✅ Full analysis (via AI chat) | Backend API |
|
||||
| **Whale Activity** | ✅ Full tracking | ✅ Full tracking (via AI chat) | Backend API |
|
||||
| **Trading Suggestions** | ✅ Full suggestions | ✅ Full suggestions (via AI chat) | Backend API |
|
||||
| **Portfolio Tracker** | ✅ Full tracking | ✅ Full tracking | Backend API |
|
||||
| **Chart Capture** | ✅ Full capture + annotations | ❌ N/A (Extension-only) | Local |
|
||||
| **Thread Generator** | ✅ Full generation | ❌ N/A (Extension-only) | Local |
|
||||
| **Context Detection** | ✅ Auto-detect tokens | ❌ N/A (Extension-only) | Local |
|
||||
| **Floating Button** | ✅ Quick access | ❌ N/A (Extension-only) | Local |
|
||||
| **Settings** | 📖 Quick settings | ✏️ Full settings | Backend API |
|
||||
| **Analytics** | 👁️ Basic stats | ✅ Full analytics | Backend API |
|
||||
|
||||
**Legend:**
|
||||
- ✅ Full feature
|
||||
- ✅ Full feature (create, edit, delete, view)
|
||||
- ✏️ Full control (create, edit, delete)
|
||||
- 📖 Read-only (view/select only)
|
||||
- 👁️ View only
|
||||
- ❌ N/A (Not applicable)
|
||||
|
||||
**Extension-only Features:**
|
||||
- Chart Capture with Annotations (cần truy cập DOM của trang)
|
||||
- AI Thread Generator (tối ưu cho workflow trên browser)
|
||||
- Context Detection (cần content script)
|
||||
- Floating Quick Action Button (cần inject vào trang)
|
||||
|
||||
**Web Dashboard Focus:**
|
||||
- Full Settings Management
|
||||
- Detailed Analytics & Reports
|
||||
- Connector Setup & Configuration
|
||||
- Document Management
|
||||
|
||||
#### Web Dashboard (Secondary - Existing)
|
||||
* **[FR-UI-01] Chat Management:** Xem lịch sử chat, manage search spaces.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,122 @@ chrome.sidePanel
|
|||
.setPanelBehavior({ openPanelOnActionClick: true })
|
||||
.catch((error) => console.error("Failed to set side panel behavior:", error));
|
||||
|
||||
// ============================================
|
||||
// Context Menu Setup (Epic 4.3)
|
||||
// ============================================
|
||||
|
||||
// Create context menus on extension install
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
// Parent menu for SurfSense
|
||||
chrome.contextMenus.create({
|
||||
id: "surfsense-parent",
|
||||
title: "🧠 SurfSense",
|
||||
contexts: ["selection", "page", "link"],
|
||||
});
|
||||
|
||||
// Analyze Token - for selected text (token address or symbol)
|
||||
chrome.contextMenus.create({
|
||||
id: "analyze-token",
|
||||
parentId: "surfsense-parent",
|
||||
title: "🔍 Analyze Token",
|
||||
contexts: ["selection"],
|
||||
});
|
||||
|
||||
// Check Safety - for selected text
|
||||
chrome.contextMenus.create({
|
||||
id: "check-safety",
|
||||
parentId: "surfsense-parent",
|
||||
title: "🛡️ Check Safety",
|
||||
contexts: ["selection"],
|
||||
});
|
||||
|
||||
// Add to Watchlist - for selected text
|
||||
chrome.contextMenus.create({
|
||||
id: "add-watchlist",
|
||||
parentId: "surfsense-parent",
|
||||
title: "⭐ Add to Watchlist",
|
||||
contexts: ["selection"],
|
||||
});
|
||||
|
||||
// Separator
|
||||
chrome.contextMenus.create({
|
||||
id: "separator-1",
|
||||
parentId: "surfsense-parent",
|
||||
type: "separator",
|
||||
contexts: ["selection", "page", "link"],
|
||||
});
|
||||
|
||||
// Copy Address - for selected text
|
||||
chrome.contextMenus.create({
|
||||
id: "copy-address",
|
||||
parentId: "surfsense-parent",
|
||||
title: "📋 Copy Address",
|
||||
contexts: ["selection"],
|
||||
});
|
||||
|
||||
// View on Explorer - for selected text
|
||||
chrome.contextMenus.create({
|
||||
id: "view-explorer",
|
||||
parentId: "surfsense-parent",
|
||||
title: "🔗 View on Explorer",
|
||||
contexts: ["selection"],
|
||||
});
|
||||
|
||||
// Separator
|
||||
chrome.contextMenus.create({
|
||||
id: "separator-2",
|
||||
parentId: "surfsense-parent",
|
||||
type: "separator",
|
||||
contexts: ["selection", "page", "link"],
|
||||
});
|
||||
|
||||
// Capture Page - for page context
|
||||
chrome.contextMenus.create({
|
||||
id: "capture-page",
|
||||
parentId: "surfsense-parent",
|
||||
title: "📸 Capture This Page",
|
||||
contexts: ["page"],
|
||||
});
|
||||
|
||||
// Ask AI about this page
|
||||
chrome.contextMenus.create({
|
||||
id: "ask-ai-page",
|
||||
parentId: "surfsense-parent",
|
||||
title: "💬 Ask AI About This Page",
|
||||
contexts: ["page"],
|
||||
});
|
||||
});
|
||||
|
||||
// Handle context menu clicks
|
||||
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||
const selectedText = info.selectionText?.trim() || "";
|
||||
const storage = new Storage({ area: "local" });
|
||||
|
||||
// Store the action for sidepanel to pick up
|
||||
const contextAction = {
|
||||
action: info.menuItemId,
|
||||
text: selectedText,
|
||||
pageUrl: info.pageUrl,
|
||||
linkUrl: info.linkUrl,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
await storage.set("pendingContextAction", contextAction);
|
||||
|
||||
// Open sidepanel to handle the action
|
||||
if (tab?.id) {
|
||||
try {
|
||||
await chrome.sidePanel.open({ tabId: tab.id });
|
||||
} catch (error) {
|
||||
console.error("Failed to open side panel:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Message Listeners
|
||||
// ============================================
|
||||
|
||||
// Listen for messages from content scripts
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === "OPEN_SIDEPANEL") {
|
||||
|
|
@ -16,6 +132,43 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||
.catch((error) => console.error("Failed to open side panel:", error));
|
||||
}
|
||||
}
|
||||
|
||||
// Handle context action from sidepanel
|
||||
if (message.type === "GET_CONTEXT_ACTION") {
|
||||
const storage = new Storage({ area: "local" });
|
||||
storage.get("pendingContextAction").then((action) => {
|
||||
sendResponse(action);
|
||||
// Clear the pending action
|
||||
storage.remove("pendingContextAction");
|
||||
});
|
||||
return true; // Keep channel open for async response
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Keyboard Shortcuts (Epic 4.5)
|
||||
// ============================================
|
||||
|
||||
chrome.commands.onCommand.addListener(async (command) => {
|
||||
const storage = new Storage({ area: "local" });
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
if (!tab?.id) return;
|
||||
|
||||
// Store the keyboard command for sidepanel to pick up
|
||||
const keyboardAction = {
|
||||
action: command,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
await storage.set("pendingKeyboardAction", keyboardAction);
|
||||
|
||||
// Open sidepanel for all commands
|
||||
try {
|
||||
await chrome.sidePanel.open({ tabId: tab.id });
|
||||
} catch (error) {
|
||||
console.error("Failed to open side panel:", error);
|
||||
}
|
||||
});
|
||||
|
||||
chrome.tabs.onCreated.addListener(async (tab: any) => {
|
||||
|
|
|
|||
|
|
@ -68,13 +68,44 @@
|
|||
],
|
||||
"name": "SurfSense",
|
||||
"description": "Extension to collect Browsing History for SurfSense.",
|
||||
"version": "0.0.3"
|
||||
"version": "0.0.3",
|
||||
"commands": {
|
||||
"analyze-token": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Shift+A",
|
||||
"mac": "Command+Shift+A"
|
||||
},
|
||||
"description": "Analyze current token"
|
||||
},
|
||||
"add-watchlist": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Shift+W",
|
||||
"mac": "Command+Shift+W"
|
||||
},
|
||||
"description": "Add token to watchlist"
|
||||
},
|
||||
"capture-page": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Shift+E",
|
||||
"mac": "Command+Shift+E"
|
||||
},
|
||||
"description": "Capture current page"
|
||||
},
|
||||
"show-portfolio": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Shift+P",
|
||||
"mac": "Command+Shift+P"
|
||||
},
|
||||
"description": "Show portfolio"
|
||||
}
|
||||
}
|
||||
},
|
||||
"permissions": [
|
||||
"storage",
|
||||
"scripting",
|
||||
"unlimitedStorage",
|
||||
"activeTab",
|
||||
"sidePanel"
|
||||
"sidePanel",
|
||||
"contextMenus"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { usePageContext } from "../context/PageContextProvider";
|
||||
import { TokenInfoCard } from "../dexscreener/TokenInfoCard";
|
||||
import { QuickCapture } from "./QuickCapture";
|
||||
|
|
@ -22,6 +22,8 @@ import { SafetyScoreDisplay } from "../crypto/SafetyScoreDisplay";
|
|||
import { WatchlistPanel } from "../crypto/WatchlistPanel";
|
||||
import { AlertConfigModal } from "../crypto/AlertConfigModal";
|
||||
import { DetectedTokensList } from "../components/DetectedTokensList";
|
||||
import { useContextAction, getMessageForAction } from "../hooks/useContextAction";
|
||||
import { useKeyboardShortcuts, getMessageForKeyboardAction } from "../hooks/useKeyboardShortcuts";
|
||||
import type { WatchlistItem } from "../widgets";
|
||||
import type { TokenData } from "../context/PageContextProvider";
|
||||
|
||||
|
|
@ -78,9 +80,39 @@ export function ChatInterface() {
|
|||
const [watchlistTokens, setWatchlistTokens] = useState(MOCK_WATCHLIST_TOKENS);
|
||||
const [isInWatchlist, setIsInWatchlist] = useState(false);
|
||||
|
||||
// Context menu action hook
|
||||
const { pendingAction, clearAction } = useContextAction();
|
||||
|
||||
// Keyboard shortcuts hook
|
||||
const { pendingAction: pendingKeyboardAction, clearAction: clearKeyboardAction } = useKeyboardShortcuts();
|
||||
|
||||
// Mock user data - in production, this would come from auth context
|
||||
const userName = "Crypto Trader";
|
||||
|
||||
// Handle context menu actions
|
||||
useEffect(() => {
|
||||
if (pendingAction) {
|
||||
const message = getMessageForAction(pendingAction);
|
||||
if (message) {
|
||||
// Auto-send the message
|
||||
handleSendMessage(message);
|
||||
}
|
||||
clearAction();
|
||||
}
|
||||
}, [pendingAction, clearAction]);
|
||||
|
||||
// Handle keyboard shortcut actions
|
||||
useEffect(() => {
|
||||
if (pendingKeyboardAction) {
|
||||
const message = getMessageForKeyboardAction(pendingKeyboardAction);
|
||||
if (message) {
|
||||
// Auto-send the message
|
||||
handleSendMessage(message);
|
||||
}
|
||||
clearKeyboardAction();
|
||||
}
|
||||
}, [pendingKeyboardAction, clearKeyboardAction]);
|
||||
|
||||
const handleSendMessage = async (content: string, attachments?: AttachedFile[]) => {
|
||||
console.log("Sending message:", content, attachments);
|
||||
setIsStreaming(true);
|
||||
|
|
|
|||
4
surfsense_browser_extension/sidepanel/hooks/index.ts
Normal file
4
surfsense_browser_extension/sidepanel/hooks/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// Hooks for SurfSense Browser Extension
|
||||
|
||||
export { useContextAction, getMessageForAction, type ContextAction } from "./useContextAction";
|
||||
export { useKeyboardShortcuts, getMessageForKeyboardAction, type KeyboardAction } from "./useKeyboardShortcuts";
|
||||
104
surfsense_browser_extension/sidepanel/hooks/useContextAction.ts
Normal file
104
surfsense_browser_extension/sidepanel/hooks/useContextAction.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { useEffect, useState, useCallback } from "react";
|
||||
import { Storage } from "@plasmohq/storage";
|
||||
|
||||
export interface ContextAction {
|
||||
action: string;
|
||||
text: string;
|
||||
pageUrl?: string;
|
||||
linkUrl?: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to handle context menu actions from background script
|
||||
* Returns pending action and a function to clear it
|
||||
*/
|
||||
export function useContextAction() {
|
||||
const [pendingAction, setPendingAction] = useState<ContextAction | null>(null);
|
||||
|
||||
// Check for pending context action on mount and when sidepanel gains focus
|
||||
const checkPendingAction = useCallback(async () => {
|
||||
const storage = new Storage({ area: "local" });
|
||||
const action = await storage.get<ContextAction>("pendingContextAction");
|
||||
|
||||
if (action && action.timestamp) {
|
||||
// Only process actions from last 30 seconds
|
||||
const isRecent = Date.now() - action.timestamp < 30000;
|
||||
if (isRecent) {
|
||||
setPendingAction(action);
|
||||
// Clear the pending action
|
||||
await storage.remove("pendingContextAction");
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Check on mount
|
||||
checkPendingAction();
|
||||
|
||||
// Check when window gains focus (sidepanel opened)
|
||||
const handleFocus = () => {
|
||||
checkPendingAction();
|
||||
};
|
||||
|
||||
window.addEventListener("focus", handleFocus);
|
||||
|
||||
// Also listen for visibility change
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
checkPendingAction();
|
||||
}
|
||||
};
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("focus", handleFocus);
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
};
|
||||
}, [checkPendingAction]);
|
||||
|
||||
const clearAction = useCallback(() => {
|
||||
setPendingAction(null);
|
||||
}, []);
|
||||
|
||||
return { pendingAction, clearAction, checkPendingAction };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate chat message based on context action
|
||||
*/
|
||||
export function getMessageForAction(action: ContextAction): string | null {
|
||||
const text = action.text;
|
||||
|
||||
switch (action.action) {
|
||||
case "analyze-token":
|
||||
return `Analyze token: ${text}`;
|
||||
case "check-safety":
|
||||
return `Is ${text} safe? Check for rug pull risks.`;
|
||||
case "add-watchlist":
|
||||
return `Add ${text} to my watchlist`;
|
||||
case "copy-address":
|
||||
// This is handled differently - just copy to clipboard
|
||||
if (text) {
|
||||
navigator.clipboard.writeText(text);
|
||||
}
|
||||
return null;
|
||||
case "view-explorer":
|
||||
// Detect chain and open explorer
|
||||
if (text.startsWith("0x") && text.length === 42) {
|
||||
// Ethereum address
|
||||
window.open(`https://etherscan.io/address/${text}`, "_blank");
|
||||
} else if (text.length >= 32 && text.length <= 44) {
|
||||
// Solana address
|
||||
window.open(`https://solscan.io/account/${text}`, "_blank");
|
||||
}
|
||||
return null;
|
||||
case "capture-page":
|
||||
return "Capture this page to my knowledge base";
|
||||
case "ask-ai-page":
|
||||
return "What is this page about? Summarize the key information.";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import { useEffect, useState, useCallback } from "react";
|
||||
import { Storage } from "@plasmohq/storage";
|
||||
|
||||
export interface KeyboardAction {
|
||||
action: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to handle keyboard shortcut actions from background script
|
||||
* Returns pending action and a function to clear it
|
||||
*
|
||||
* Keyboard shortcuts defined in manifest:
|
||||
* - open-sidepanel: Ctrl+Shift+S (just opens panel, no message)
|
||||
* - analyze-token: Ctrl+Shift+A
|
||||
* - add-watchlist: Ctrl+Shift+W
|
||||
* - capture-page: Ctrl+Shift+C
|
||||
* - show-portfolio: Ctrl+Shift+P
|
||||
*/
|
||||
export function useKeyboardShortcuts() {
|
||||
const [pendingAction, setPendingAction] = useState<KeyboardAction | null>(null);
|
||||
|
||||
// Check for pending keyboard action on mount and when sidepanel gains focus
|
||||
const checkPendingAction = useCallback(async () => {
|
||||
const storage = new Storage({ area: "local" });
|
||||
const action = await storage.get<KeyboardAction>("pendingKeyboardAction");
|
||||
|
||||
if (action && action.timestamp) {
|
||||
// Only process actions from last 30 seconds
|
||||
const isRecent = Date.now() - action.timestamp < 30000;
|
||||
if (isRecent) {
|
||||
setPendingAction(action);
|
||||
// Clear the pending action
|
||||
await storage.remove("pendingKeyboardAction");
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Check on mount
|
||||
checkPendingAction();
|
||||
|
||||
// Check when window gains focus (sidepanel opened)
|
||||
const handleFocus = () => {
|
||||
checkPendingAction();
|
||||
};
|
||||
|
||||
window.addEventListener("focus", handleFocus);
|
||||
|
||||
// Also listen for visibility change
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
checkPendingAction();
|
||||
}
|
||||
};
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("focus", handleFocus);
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
};
|
||||
}, [checkPendingAction]);
|
||||
|
||||
const clearAction = useCallback(() => {
|
||||
setPendingAction(null);
|
||||
}, []);
|
||||
|
||||
return { pendingAction, clearAction, checkPendingAction };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate chat message based on keyboard shortcut action
|
||||
* Returns null for actions that don't need a chat message (like open-sidepanel)
|
||||
*/
|
||||
export function getMessageForKeyboardAction(action: KeyboardAction): string | null {
|
||||
switch (action.action) {
|
||||
case "open-sidepanel":
|
||||
// Just opens the panel, no message needed
|
||||
return null;
|
||||
case "analyze-token":
|
||||
return "Analyze the current token on this page";
|
||||
case "add-watchlist":
|
||||
return "Add the current token to my watchlist";
|
||||
case "capture-page":
|
||||
return "Capture this page to my knowledge base";
|
||||
case "show-portfolio":
|
||||
return "Show my portfolio";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
import { useState } from "react";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { Bell, BellOff, Volume2, VolumeX, Clock, Filter, ChevronRight } from "lucide-react";
|
||||
import { Button } from "@/routes/ui/button";
|
||||
|
||||
export interface NotificationSettings {
|
||||
enabled: boolean;
|
||||
sound: boolean;
|
||||
quietHoursEnabled: boolean;
|
||||
quietHoursStart: string;
|
||||
quietHoursEnd: string;
|
||||
groupNotifications: boolean;
|
||||
priorities: {
|
||||
high: boolean;
|
||||
medium: boolean;
|
||||
low: boolean;
|
||||
};
|
||||
categories: {
|
||||
priceAlerts: boolean;
|
||||
whaleActivity: boolean;
|
||||
rugPullWarnings: boolean;
|
||||
portfolioUpdates: boolean;
|
||||
newsAlerts: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NotificationSettingsPanelProps {
|
||||
settings: NotificationSettings;
|
||||
onSettingsChange: (settings: NotificationSettings) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: NotificationSettings = {
|
||||
enabled: true,
|
||||
sound: true,
|
||||
quietHoursEnabled: false,
|
||||
quietHoursStart: "22:00",
|
||||
quietHoursEnd: "08:00",
|
||||
groupNotifications: true,
|
||||
priorities: {
|
||||
high: true,
|
||||
medium: true,
|
||||
low: false,
|
||||
},
|
||||
categories: {
|
||||
priceAlerts: true,
|
||||
whaleActivity: true,
|
||||
rugPullWarnings: true,
|
||||
portfolioUpdates: true,
|
||||
newsAlerts: false,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* NotificationSettingsPanel - Configure notification preferences
|
||||
* Part of Epic 4.4 - Smart Notifications
|
||||
*/
|
||||
export function NotificationSettingsPanel({
|
||||
settings = DEFAULT_SETTINGS,
|
||||
onSettingsChange,
|
||||
className,
|
||||
}: NotificationSettingsPanelProps) {
|
||||
const updateSettings = (partial: Partial<NotificationSettings>) => {
|
||||
onSettingsChange({ ...settings, ...partial });
|
||||
};
|
||||
|
||||
const updatePriority = (key: keyof NotificationSettings["priorities"], value: boolean) => {
|
||||
onSettingsChange({
|
||||
...settings,
|
||||
priorities: { ...settings.priorities, [key]: value },
|
||||
});
|
||||
};
|
||||
|
||||
const updateCategory = (key: keyof NotificationSettings["categories"], value: boolean) => {
|
||||
onSettingsChange({
|
||||
...settings,
|
||||
categories: { ...settings.categories, [key]: value },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("rounded-lg border bg-card p-4 space-y-4", className)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bell className="h-5 w-5 text-primary" />
|
||||
<span className="font-medium">Notification Settings</span>
|
||||
</div>
|
||||
<Button
|
||||
variant={settings.enabled ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => updateSettings({ enabled: !settings.enabled })}
|
||||
>
|
||||
{settings.enabled ? (
|
||||
<>
|
||||
<Bell className="h-4 w-4 mr-1" /> On
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<BellOff className="h-4 w-4 mr-1" /> Off
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{settings.enabled && (
|
||||
<>
|
||||
{/* Sound Toggle */}
|
||||
<div className="flex items-center justify-between py-2 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
{settings.sound ? (
|
||||
<Volume2 className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<VolumeX className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
<span className="text-sm">Sound</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => updateSettings({ sound: !settings.sound })}
|
||||
>
|
||||
{settings.sound ? "On" : "Off"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Quiet Hours */}
|
||||
<div className="space-y-2 py-2 border-b">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm">Quiet Hours</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => updateSettings({ quietHoursEnabled: !settings.quietHoursEnabled })}
|
||||
>
|
||||
{settings.quietHoursEnabled ? "On" : "Off"}
|
||||
</Button>
|
||||
</div>
|
||||
{settings.quietHoursEnabled && (
|
||||
<div className="flex items-center gap-2 ml-6 text-xs text-muted-foreground">
|
||||
<input
|
||||
type="time"
|
||||
value={settings.quietHoursStart}
|
||||
onChange={(e) => updateSettings({ quietHoursStart: e.target.value })}
|
||||
className="bg-muted rounded px-2 py-1"
|
||||
/>
|
||||
<span>to</span>
|
||||
<input
|
||||
type="time"
|
||||
value={settings.quietHoursEnd}
|
||||
onChange={(e) => updateSettings({ quietHoursEnd: e.target.value })}
|
||||
className="bg-muted rounded px-2 py-1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Priority Levels */}
|
||||
<div className="space-y-2 py-2 border-b">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Filter className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Priority Levels</span>
|
||||
</div>
|
||||
<div className="space-y-1 ml-6">
|
||||
{(["high", "medium", "low"] as const).map((priority) => (
|
||||
<label key={priority} className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.priorities[priority]}
|
||||
onChange={(e) => updatePriority(priority, e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className={cn(
|
||||
"text-xs capitalize",
|
||||
priority === "high" && "text-red-500",
|
||||
priority === "medium" && "text-yellow-500",
|
||||
priority === "low" && "text-muted-foreground"
|
||||
)}>
|
||||
{priority}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="space-y-2 py-2">
|
||||
<span className="text-sm font-medium">Categories</span>
|
||||
<div className="space-y-1">
|
||||
{Object.entries(settings.categories).map(([key, value]) => (
|
||||
<label key={key} className="flex items-center justify-between cursor-pointer py-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase())}
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value}
|
||||
onChange={(e) => updateCategory(key as keyof NotificationSettings["categories"], e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Group Notifications */}
|
||||
<div className="flex items-center justify-between pt-2 border-t">
|
||||
<span className="text-sm">Group similar notifications</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => updateSettings({ groupNotifications: !settings.groupNotifications })}
|
||||
>
|
||||
{settings.groupNotifications ? "On" : "Off"}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
import { cn } from "~/lib/utils";
|
||||
import { Bell, AlertTriangle, TrendingUp, TrendingDown, Wallet, Fish, X, Check } from "lucide-react";
|
||||
import { Button } from "@/routes/ui/button";
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
type: "price_alert" | "whale_activity" | "rug_warning" | "portfolio" | "news";
|
||||
priority: "high" | "medium" | "low";
|
||||
title: string;
|
||||
message: string;
|
||||
tokenSymbol?: string;
|
||||
timestamp: Date;
|
||||
read: boolean;
|
||||
actionUrl?: string;
|
||||
}
|
||||
|
||||
export interface NotificationsListProps {
|
||||
notifications: Notification[];
|
||||
onMarkRead: (id: string) => void;
|
||||
onMarkAllRead: () => void;
|
||||
onDismiss: (id: string) => void;
|
||||
onNotificationClick?: (notification: Notification) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const getNotificationIcon = (type: Notification["type"]) => {
|
||||
switch (type) {
|
||||
case "price_alert":
|
||||
return <TrendingUp className="h-4 w-4" />;
|
||||
case "whale_activity":
|
||||
return <Fish className="h-4 w-4" />;
|
||||
case "rug_warning":
|
||||
return <AlertTriangle className="h-4 w-4" />;
|
||||
case "portfolio":
|
||||
return <Wallet className="h-4 w-4" />;
|
||||
case "news":
|
||||
return <Bell className="h-4 w-4" />;
|
||||
default:
|
||||
return <Bell className="h-4 w-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: Notification["priority"]) => {
|
||||
switch (priority) {
|
||||
case "high":
|
||||
return "border-l-red-500 bg-red-500/5";
|
||||
case "medium":
|
||||
return "border-l-yellow-500 bg-yellow-500/5";
|
||||
case "low":
|
||||
return "border-l-muted-foreground bg-muted/30";
|
||||
default:
|
||||
return "border-l-muted-foreground";
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (date: Date): string => {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(diff / 86400000);
|
||||
|
||||
if (minutes < 1) return "Just now";
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
return `${days}d ago`;
|
||||
};
|
||||
|
||||
/**
|
||||
* NotificationsList - Display and manage notifications
|
||||
* Part of Epic 4.4 - Smart Notifications
|
||||
*/
|
||||
export function NotificationsList({
|
||||
notifications,
|
||||
onMarkRead,
|
||||
onMarkAllRead,
|
||||
onDismiss,
|
||||
onNotificationClick,
|
||||
className,
|
||||
}: NotificationsListProps) {
|
||||
const unreadCount = notifications.filter((n) => !n.read).length;
|
||||
|
||||
return (
|
||||
<div className={cn("rounded-lg border bg-card", className)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-3 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bell className="h-4 w-4 text-primary" />
|
||||
<span className="font-medium text-sm">Notifications</span>
|
||||
{unreadCount > 0 && (
|
||||
<span className="bg-primary text-primary-foreground text-xs px-1.5 py-0.5 rounded-full">
|
||||
{unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{unreadCount > 0 && (
|
||||
<Button variant="ghost" size="sm" className="text-xs" onClick={onMarkAllRead}>
|
||||
<Check className="h-3 w-3 mr-1" />
|
||||
Mark all read
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Notifications List */}
|
||||
<div className="max-h-[400px] overflow-y-auto">
|
||||
{notifications.length === 0 ? (
|
||||
<div className="p-8 text-center text-muted-foreground text-sm">
|
||||
<Bell className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||
<p>No notifications yet</p>
|
||||
</div>
|
||||
) : (
|
||||
notifications.map((notification) => (
|
||||
<div
|
||||
key={notification.id}
|
||||
className={cn(
|
||||
"flex items-start gap-3 p-3 border-b border-l-2 cursor-pointer hover:bg-muted/50 transition-colors",
|
||||
getPriorityColor(notification.priority),
|
||||
!notification.read && "bg-primary/5"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!notification.read) onMarkRead(notification.id);
|
||||
onNotificationClick?.(notification);
|
||||
}}
|
||||
>
|
||||
<div className={cn(
|
||||
"p-1.5 rounded-full",
|
||||
notification.type === "rug_warning" ? "bg-red-500/20 text-red-500" :
|
||||
notification.type === "whale_activity" ? "bg-blue-500/20 text-blue-500" :
|
||||
notification.type === "price_alert" ? "bg-green-500/20 text-green-500" :
|
||||
"bg-muted text-muted-foreground"
|
||||
)}>
|
||||
{getNotificationIcon(notification.type)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn("text-sm font-medium", !notification.read && "text-foreground")}>
|
||||
{notification.title}
|
||||
</span>
|
||||
{notification.tokenSymbol && (
|
||||
<span className="text-xs bg-muted px-1.5 py-0.5 rounded">
|
||||
{notification.tokenSymbol}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground line-clamp-2 mt-0.5">
|
||||
{notification.message}
|
||||
</p>
|
||||
<span className="text-[10px] text-muted-foreground mt-1 block">
|
||||
{formatTime(notification.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 opacity-0 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDismiss(notification.id);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
6
surfsense_browser_extension/sidepanel/settings/index.ts
Normal file
6
surfsense_browser_extension/sidepanel/settings/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Settings components for SurfSense Browser Extension
|
||||
|
||||
export { NotificationSettingsPanel, type NotificationSettings, type NotificationSettingsPanelProps } from "./NotificationSettingsPanel";
|
||||
export { NotificationsList, type Notification, type NotificationsListProps } from "./NotificationsList";
|
||||
export { ProductivitySettings } from "./ProductivitySettings";
|
||||
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
import { cn } from "~/lib/utils";
|
||||
import { Users, AlertTriangle, Crown } from "lucide-react";
|
||||
import { ChainIcon } from "../components/shared/ChainIcon";
|
||||
|
||||
export interface Holder {
|
||||
rank: number;
|
||||
address: string;
|
||||
label?: string;
|
||||
balance: number;
|
||||
percentage: number;
|
||||
isContract?: boolean;
|
||||
}
|
||||
|
||||
export interface HolderAnalysisData {
|
||||
tokenSymbol: string;
|
||||
chain: string;
|
||||
totalHolders: number;
|
||||
top10Percentage: number;
|
||||
top50Percentage?: number;
|
||||
holders: Holder[];
|
||||
concentrationRisk?: "low" | "medium" | "high" | "critical";
|
||||
}
|
||||
|
||||
export interface HolderAnalysisWidgetProps {
|
||||
/** Holder analysis data */
|
||||
data: HolderAnalysisData;
|
||||
/** Callback when holder is clicked */
|
||||
onHolderClick?: (holder: Holder) => void;
|
||||
/** Additional class names */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* HolderAnalysisWidget - Displays holder distribution inline in chat
|
||||
* Used when AI responds to "who holds BULLA?" or "analyze holders"
|
||||
*/
|
||||
export function HolderAnalysisWidget({
|
||||
data,
|
||||
onHolderClick,
|
||||
className,
|
||||
}: HolderAnalysisWidgetProps) {
|
||||
const risk = data.concentrationRisk || "medium";
|
||||
|
||||
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">
|
||||
<Users className="h-5 w-5 text-purple-500" />
|
||||
<span className="font-medium text-sm">Holder Analysis - {data.tokenSymbol}</span>
|
||||
</div>
|
||||
<ChainIcon chain={data.chain} size="sm" />
|
||||
</div>
|
||||
|
||||
{/* Summary Stats */}
|
||||
<div className="grid grid-cols-2 gap-2 mb-3">
|
||||
<div className="bg-muted/50 rounded p-2">
|
||||
<p className="text-xs text-muted-foreground">Total Holders</p>
|
||||
<p className="font-medium text-sm">{data.totalHolders.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className={cn("rounded p-2", data.top10Percentage > 50 ? "bg-red-500/10" : "bg-muted/50")}>
|
||||
<p className="text-xs text-muted-foreground">Top 10 Hold</p>
|
||||
<p className={cn("font-medium text-sm", data.top10Percentage > 50 && "text-red-500")}>
|
||||
{data.top10Percentage.toFixed(1)}%
|
||||
</p>
|
||||
</div>
|
||||
{data.top50Percentage && (
|
||||
<div className="bg-muted/50 rounded p-2">
|
||||
<p className="text-xs text-muted-foreground">Top 50 Hold</p>
|
||||
<p className="font-medium text-sm">{data.top50Percentage.toFixed(1)}%</p>
|
||||
</div>
|
||||
)}
|
||||
<div className={cn("rounded p-2", getRiskColor(risk))}>
|
||||
<p className="text-xs text-muted-foreground">Concentration Risk</p>
|
||||
<p className="font-medium text-sm capitalize">{risk}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Risk Warning */}
|
||||
{(risk === "high" || risk === "critical") && (
|
||||
<div className="flex items-center gap-2 text-yellow-600 dark:text-yellow-400 text-xs bg-yellow-500/10 rounded-lg p-2 mb-3">
|
||||
<AlertTriangle className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span>High holder concentration. Top wallets could impact price.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Top Holders List */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-medium text-muted-foreground mb-2">Top Holders</p>
|
||||
<div className="divide-y max-h-[200px] overflow-y-auto">
|
||||
{data.holders.slice(0, 10).map((holder) => (
|
||||
<div
|
||||
key={holder.address}
|
||||
className="flex items-center justify-between py-2 hover:bg-muted/50 -mx-2 px-2 rounded cursor-pointer transition-colors"
|
||||
onClick={() => onHolderClick?.(holder)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-bold text-muted-foreground w-5">#{holder.rank}</span>
|
||||
{holder.rank <= 3 && (
|
||||
<Crown className={cn(
|
||||
"h-3.5 w-3.5",
|
||||
holder.rank === 1 ? "text-yellow-500" :
|
||||
holder.rank === 2 ? "text-gray-400" : "text-amber-600"
|
||||
)} />
|
||||
)}
|
||||
<div>
|
||||
<p className="font-medium text-xs">{holder.label || shortenAddress(holder.address)}</p>
|
||||
{holder.isContract && (
|
||||
<span className="text-[10px] bg-muted px-1 rounded">Contract</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium text-xs">{holder.percentage.toFixed(2)}%</p>
|
||||
<p className="text-[10px] text-muted-foreground">{formatBalance(holder.balance)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
import { cn } from "~/lib/utils";
|
||||
import { Activity, TrendingUp, TrendingDown, RefreshCw, ExternalLink, Droplets, BarChart3 } from "lucide-react";
|
||||
import { Button } from "@/routes/ui/button";
|
||||
import { ChainIcon } from "../components/shared/ChainIcon";
|
||||
|
||||
export interface LiveTokenDataInfo {
|
||||
chain: string;
|
||||
tokenAddress: string;
|
||||
tokenSymbol?: string;
|
||||
tokenName?: string;
|
||||
priceUsd?: string;
|
||||
priceNative?: string;
|
||||
priceChange5m?: number;
|
||||
priceChange1h?: number;
|
||||
priceChange6h?: number;
|
||||
priceChange24h?: number;
|
||||
volume24h?: number;
|
||||
volume6h?: number;
|
||||
volume1h?: number;
|
||||
liquidityUsd?: number;
|
||||
marketCap?: number;
|
||||
fdv?: number;
|
||||
txns24hBuys?: number;
|
||||
txns24hSells?: number;
|
||||
dex?: string;
|
||||
pairUrl?: string;
|
||||
totalPairs?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface LiveTokenDataWidgetProps {
|
||||
/** Live token data */
|
||||
data: LiveTokenDataInfo;
|
||||
/** Whether data is loading */
|
||||
isLoading?: boolean;
|
||||
/** Callback when view on DexScreener is clicked */
|
||||
onViewDexScreener?: () => void;
|
||||
/** Additional class names */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const formatPrice = (price: string | undefined): string => {
|
||||
if (!price || price === "N/A") return "N/A";
|
||||
const num = parseFloat(price);
|
||||
if (isNaN(num)) return price;
|
||||
if (num < 0.00001) return `$${num.toExponential(2)}`;
|
||||
if (num < 1) return `$${num.toFixed(6)}`;
|
||||
return `$${num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
};
|
||||
|
||||
const formatLargeNumber = (num: number | undefined): string => {
|
||||
if (num === undefined || num === null || num === 0) return "N/A";
|
||||
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)}`;
|
||||
};
|
||||
|
||||
const formatNumber = (num: number | undefined): string => {
|
||||
if (num === undefined || num === null) return "0";
|
||||
return num.toLocaleString();
|
||||
};
|
||||
|
||||
const PriceChange = ({ value, label }: { value: number | undefined; label: string }) => {
|
||||
if (value === undefined || value === null) return null;
|
||||
const isPositive = value >= 0;
|
||||
return (
|
||||
<div className="text-center">
|
||||
<p className="text-[10px] text-muted-foreground">{label}</p>
|
||||
<p className={cn("text-xs font-medium", isPositive ? "text-green-500" : "text-red-500")}>
|
||||
{isPositive ? "+" : ""}{value.toFixed(2)}%
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* LiveTokenDataWidget - Displays comprehensive real-time market data
|
||||
* Used when AI fetches detailed live market information
|
||||
*/
|
||||
export function LiveTokenDataWidget({
|
||||
data,
|
||||
isLoading = false,
|
||||
onViewDexScreener,
|
||||
className,
|
||||
}: LiveTokenDataWidgetProps) {
|
||||
const handleOpenDexScreener = () => {
|
||||
if (onViewDexScreener) {
|
||||
onViewDexScreener();
|
||||
} else if (data.pairUrl) {
|
||||
window.open(data.pairUrl, "_blank");
|
||||
} else if (data.tokenAddress) {
|
||||
window.open(`https://dexscreener.com/${data.chain}/${data.tokenAddress}`, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
const totalTxns24h = (data.txns24hBuys || 0) + (data.txns24hSells || 0);
|
||||
const buyRatio = totalTxns24h > 0 ? ((data.txns24hBuys || 0) / totalTxns24h) * 100 : 50;
|
||||
|
||||
return (
|
||||
<div className={cn("rounded-lg border border-purple-500/20 bg-card p-4 my-2", className)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4 w-4 text-purple-500" />
|
||||
<span className="font-medium text-sm">Live Market Data</span>
|
||||
{isLoading ? (
|
||||
<span className="text-xs bg-muted px-2 py-0.5 rounded animate-pulse">Fetching...</span>
|
||||
) : (
|
||||
<span className="text-xs text-purple-500 flex items-center gap-1">
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
Real-time
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.error ? (
|
||||
<div className="text-red-500 text-xs p-2 bg-red-500/10 rounded">
|
||||
⚠️ {data.error}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Token Header */}
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<ChainIcon chain={data.chain} size="sm" />
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold">{data.tokenSymbol || "Token"}</span>
|
||||
{data.tokenName && (
|
||||
<span className="text-xs text-muted-foreground">{data.tokenName}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-lg">{formatPrice(data.priceUsd)}</span>
|
||||
{data.priceChange24h !== undefined && (
|
||||
<span className={cn(
|
||||
"flex items-center gap-0.5 text-xs font-medium",
|
||||
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(2)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Price Changes */}
|
||||
<div className="flex justify-around py-2 bg-muted/30 rounded mb-3">
|
||||
<PriceChange value={data.priceChange5m} label="5m" />
|
||||
<PriceChange value={data.priceChange1h} label="1h" />
|
||||
<PriceChange value={data.priceChange6h} label="6h" />
|
||||
<PriceChange value={data.priceChange24h} label="24h" />
|
||||
</div>
|
||||
|
||||
{/* Metrics Grid */}
|
||||
<div className="grid grid-cols-2 gap-2 mb-3">
|
||||
<div className="bg-muted/50 rounded p-2">
|
||||
<p className="text-[10px] text-muted-foreground flex items-center gap-1">
|
||||
<BarChart3 className="h-3 w-3" /> 24h Volume
|
||||
</p>
|
||||
<p className="font-medium text-sm">{formatLargeNumber(data.volume24h)}</p>
|
||||
</div>
|
||||
<div className="bg-muted/50 rounded p-2">
|
||||
<p className="text-[10px] text-muted-foreground flex items-center gap-1">
|
||||
<Droplets className="h-3 w-3" /> Liquidity
|
||||
</p>
|
||||
<p className="font-medium text-sm">{formatLargeNumber(data.liquidityUsd)}</p>
|
||||
</div>
|
||||
<div className="bg-muted/50 rounded p-2">
|
||||
<p className="text-[10px] text-muted-foreground">Market Cap</p>
|
||||
<p className="font-medium text-sm">{formatLargeNumber(data.marketCap)}</p>
|
||||
</div>
|
||||
<div className="bg-muted/50 rounded p-2">
|
||||
<p className="text-[10px] text-muted-foreground">FDV</p>
|
||||
<p className="font-medium text-sm">{formatLargeNumber(data.fdv)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Transaction Activity */}
|
||||
<div className="space-y-1 mb-3">
|
||||
<p className="text-xs font-medium flex items-center gap-1">
|
||||
<Activity className="h-3 w-3" /> 24h Transactions
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-green-500 transition-all"
|
||||
style={{ width: `${buyRatio}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-[10px]">
|
||||
<span className="text-green-500">{formatNumber(data.txns24hBuys)} buys</span>
|
||||
<span className="text-muted-foreground">{formatNumber(totalTxns24h)} total</span>
|
||||
<span className="text-red-500">{formatNumber(data.txns24hSells)} sells</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* DEX Info & Actions */}
|
||||
<div className="flex items-center justify-between pt-2 border-t">
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
<span>DEX: {data.dex || "Unknown"}</span>
|
||||
{data.totalPairs && data.totalPairs > 1 && (
|
||||
<span className="ml-2">• {data.totalPairs} pairs</span>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={handleOpenDexScreener}>
|
||||
<ExternalLink className="h-3 w-3 mr-1" />
|
||||
DexScreener
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import { cn } from "~/lib/utils";
|
||||
import { Zap, TrendingUp, TrendingDown, RefreshCw, ExternalLink } from "lucide-react";
|
||||
import { Button } from "@/routes/ui/button";
|
||||
import { ChainIcon } from "../components/shared/ChainIcon";
|
||||
|
||||
export interface LiveTokenPriceData {
|
||||
chain: string;
|
||||
tokenAddress: string;
|
||||
tokenSymbol?: string;
|
||||
tokenName?: string;
|
||||
priceUsd?: string;
|
||||
priceNative?: string;
|
||||
priceChange5m?: number;
|
||||
priceChange1h?: number;
|
||||
priceChange6h?: number;
|
||||
priceChange24h?: number;
|
||||
volume24h?: number;
|
||||
liquidityUsd?: number;
|
||||
marketCap?: number;
|
||||
fdv?: number;
|
||||
dex?: string;
|
||||
pairUrl?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface LiveTokenPriceWidgetProps {
|
||||
/** Live token price data */
|
||||
data: LiveTokenPriceData;
|
||||
/** Whether data is loading */
|
||||
isLoading?: boolean;
|
||||
/** Callback when view on DexScreener is clicked */
|
||||
onViewDexScreener?: () => void;
|
||||
/** Additional class names */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const formatPrice = (price: string | undefined): string => {
|
||||
if (!price || price === "N/A") return "N/A";
|
||||
const num = parseFloat(price);
|
||||
if (isNaN(num)) return price;
|
||||
if (num < 0.00001) return `$${num.toExponential(2)}`;
|
||||
if (num < 1) return `$${num.toFixed(6)}`;
|
||||
return `$${num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
};
|
||||
|
||||
const PriceChange = ({ value, label }: { value: number | undefined; label: string }) => {
|
||||
if (value === undefined || value === null) return null;
|
||||
const isPositive = value >= 0;
|
||||
return (
|
||||
<div className="text-center">
|
||||
<p className="text-[10px] text-muted-foreground">{label}</p>
|
||||
<p className={cn("text-xs font-medium", isPositive ? "text-green-500" : "text-red-500")}>
|
||||
{isPositive ? "+" : ""}{value.toFixed(2)}%
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* LiveTokenPriceWidget - Displays real-time token price inline in chat
|
||||
* Used when AI fetches current/live price data
|
||||
*/
|
||||
export function LiveTokenPriceWidget({
|
||||
data,
|
||||
isLoading = false,
|
||||
onViewDexScreener,
|
||||
className,
|
||||
}: LiveTokenPriceWidgetProps) {
|
||||
const handleOpenDexScreener = () => {
|
||||
if (onViewDexScreener) {
|
||||
onViewDexScreener();
|
||||
} else if (data.pairUrl) {
|
||||
window.open(data.pairUrl, "_blank");
|
||||
} else if (data.tokenAddress) {
|
||||
window.open(`https://dexscreener.com/${data.chain}/${data.tokenAddress}`, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("rounded-lg border border-blue-500/20 bg-card p-4 my-2", className)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="h-4 w-4 text-blue-500" />
|
||||
<span className="font-medium text-sm">Live Price</span>
|
||||
{isLoading ? (
|
||||
<span className="text-xs bg-muted px-2 py-0.5 rounded animate-pulse">Fetching...</span>
|
||||
) : (
|
||||
<span className="text-xs text-blue-500 flex items-center gap-1">
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
Real-time
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.error ? (
|
||||
<div className="text-red-500 text-xs p-2 bg-red-500/10 rounded">
|
||||
⚠️ {data.error}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Token Header */}
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<ChainIcon chain={data.chain} size="sm" />
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold">{data.tokenSymbol || "Token"}</span>
|
||||
{data.tokenName && (
|
||||
<span className="text-xs text-muted-foreground">{data.tokenName}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-lg">{formatPrice(data.priceUsd)}</span>
|
||||
{data.priceChange24h !== undefined && (
|
||||
<span className={cn(
|
||||
"flex items-center gap-0.5 text-xs font-medium",
|
||||
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(2)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Price Changes */}
|
||||
<div className="flex justify-around py-2 bg-muted/30 rounded mb-3">
|
||||
<PriceChange value={data.priceChange5m} label="5m" />
|
||||
<PriceChange value={data.priceChange1h} label="1h" />
|
||||
<PriceChange value={data.priceChange6h} label="6h" />
|
||||
<PriceChange value={data.priceChange24h} label="24h" />
|
||||
</div>
|
||||
|
||||
{/* Action */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full text-xs"
|
||||
onClick={handleOpenDexScreener}
|
||||
>
|
||||
<ExternalLink className="h-3 w-3 mr-1" />
|
||||
View on DexScreener
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
import { cn } from "~/lib/utils";
|
||||
import { Globe, TrendingUp, TrendingDown } from "lucide-react";
|
||||
|
||||
export interface MarketToken {
|
||||
symbol: string;
|
||||
name: string;
|
||||
price: number;
|
||||
priceChange24h: number;
|
||||
marketCap?: number;
|
||||
volume24h?: number;
|
||||
}
|
||||
|
||||
export interface MarketOverviewData {
|
||||
tokens: MarketToken[];
|
||||
totalMarketCap?: number;
|
||||
totalVolume24h?: number;
|
||||
btcDominance?: number;
|
||||
fearGreedIndex?: number;
|
||||
}
|
||||
|
||||
export interface MarketOverviewWidgetProps {
|
||||
/** Market overview data */
|
||||
data: MarketOverviewData;
|
||||
/** Callback when token is clicked */
|
||||
onTokenClick?: (token: MarketToken) => void;
|
||||
/** Additional class names */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
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)}`;
|
||||
};
|
||||
|
||||
const getFearGreedLabel = (index: number): string => {
|
||||
if (index > 75) return "Extreme Greed";
|
||||
if (index > 50) return "Greed";
|
||||
if (index > 25) return "Fear";
|
||||
return "Extreme Fear";
|
||||
};
|
||||
|
||||
/**
|
||||
* MarketOverviewWidget - Displays market overview inline in chat
|
||||
* Used when AI responds to "show market overview" or "how's the market?"
|
||||
*/
|
||||
export function MarketOverviewWidget({
|
||||
data,
|
||||
onTokenClick,
|
||||
className,
|
||||
}: MarketOverviewWidgetProps) {
|
||||
return (
|
||||
<div className={cn("rounded-lg border bg-card p-4 my-2", className)}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Globe className="h-5 w-5 text-blue-500" />
|
||||
<span className="font-medium text-sm">Market Overview</span>
|
||||
</div>
|
||||
|
||||
{/* Global Stats */}
|
||||
{(data.totalMarketCap || data.btcDominance || data.fearGreedIndex) && (
|
||||
<div className="grid grid-cols-2 gap-2 mb-3">
|
||||
{data.totalMarketCap && (
|
||||
<div className="bg-muted/50 rounded p-2">
|
||||
<p className="text-xs text-muted-foreground">Total Market Cap</p>
|
||||
<p className="font-medium text-sm">{formatLargeNumber(data.totalMarketCap)}</p>
|
||||
</div>
|
||||
)}
|
||||
{data.totalVolume24h && (
|
||||
<div className="bg-muted/50 rounded p-2">
|
||||
<p className="text-xs text-muted-foreground">24h Volume</p>
|
||||
<p className="font-medium text-sm">{formatLargeNumber(data.totalVolume24h)}</p>
|
||||
</div>
|
||||
)}
|
||||
{data.btcDominance && (
|
||||
<div className="bg-muted/50 rounded p-2">
|
||||
<p className="text-xs text-muted-foreground">BTC Dominance</p>
|
||||
<p className="font-medium text-sm">{data.btcDominance.toFixed(1)}%</p>
|
||||
</div>
|
||||
)}
|
||||
{data.fearGreedIndex && (
|
||||
<div className={cn(
|
||||
"rounded p-2",
|
||||
data.fearGreedIndex > 50 ? "bg-green-500/10" : "bg-red-500/10"
|
||||
)}>
|
||||
<p className="text-xs text-muted-foreground">Fear & Greed</p>
|
||||
<p className={cn(
|
||||
"font-medium text-sm",
|
||||
data.fearGreedIndex > 50 ? "text-green-500" : "text-red-500"
|
||||
)}>
|
||||
{data.fearGreedIndex} - {getFearGreedLabel(data.fearGreedIndex)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Token Prices */}
|
||||
<div className="space-y-2">
|
||||
{data.tokens.map((token) => (
|
||||
<div
|
||||
key={token.symbol}
|
||||
className="bg-muted/50 rounded p-3 flex items-center justify-between hover:bg-muted/70 cursor-pointer transition-colors"
|
||||
onClick={() => onTokenClick?.(token)}
|
||||
>
|
||||
<div>
|
||||
<p className="font-bold">{token.symbol}</p>
|
||||
<p className="text-xs text-muted-foreground">{token.name}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium">{formatPrice(token.price)}</p>
|
||||
<p className={cn(
|
||||
"text-xs flex items-center justify-end gap-0.5",
|
||||
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(2)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
import { cn } from "~/lib/utils";
|
||||
import { Flame, TrendingUp, TrendingDown, Star } from "lucide-react";
|
||||
import { Button } from "@/routes/ui/button";
|
||||
import { ChainIcon } from "../components/shared/ChainIcon";
|
||||
|
||||
export interface TrendingToken {
|
||||
symbol: string;
|
||||
name: string;
|
||||
chain: string;
|
||||
contractAddress?: string;
|
||||
price: number;
|
||||
priceChange24h: number;
|
||||
priceChange1h?: number;
|
||||
volume24h?: number;
|
||||
liquidity?: number;
|
||||
rank?: number;
|
||||
}
|
||||
|
||||
export interface TrendingTokensWidgetProps {
|
||||
/** List of trending tokens */
|
||||
tokens: TrendingToken[];
|
||||
/** Filter by chain (optional) */
|
||||
chain?: string;
|
||||
/** Timeframe for trending data */
|
||||
timeframe?: string;
|
||||
/** Callback when token is clicked */
|
||||
onTokenClick?: (token: TrendingToken) => void;
|
||||
/** Callback when add to watchlist is clicked */
|
||||
onAddToWatchlist?: (token: TrendingToken) => void;
|
||||
/** Additional class names */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
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)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* TrendingTokensWidget - Displays trending/hot tokens inline in chat
|
||||
* Used when AI responds to "what's hot on Solana?" or "show trending tokens"
|
||||
*/
|
||||
export function TrendingTokensWidget({
|
||||
tokens,
|
||||
chain = "All Chains",
|
||||
timeframe = "24h",
|
||||
onTokenClick,
|
||||
onAddToWatchlist,
|
||||
className,
|
||||
}: TrendingTokensWidgetProps) {
|
||||
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">
|
||||
<Flame className="h-5 w-5 text-orange-500" />
|
||||
<span className="font-medium text-sm">Trending on {chain}</span>
|
||||
<span className="text-xs bg-muted px-2 py-0.5 rounded">{timeframe}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Token List */}
|
||||
{tokens.length === 0 ? (
|
||||
<p className="text-muted-foreground text-center py-4 text-sm">No trending tokens found</p>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{tokens.map((token, index) => (
|
||||
<div
|
||||
key={token.symbol + index}
|
||||
className="flex items-center justify-between py-2.5 hover:bg-muted/50 -mx-2 px-2 rounded cursor-pointer transition-colors"
|
||||
onClick={() => onTokenClick?.(token)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-bold text-muted-foreground w-5">
|
||||
#{token.rank || index + 1}
|
||||
</span>
|
||||
<ChainIcon chain={token.chain} size="xs" />
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium text-sm">{token.symbol}</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{token.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-right">
|
||||
<p className="font-medium text-sm">{formatPrice(token.price)}</p>
|
||||
<p className={cn(
|
||||
"text-xs flex items-center justify-end gap-0.5",
|
||||
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)}%
|
||||
</p>
|
||||
</div>
|
||||
{token.volume24h && (
|
||||
<div className="text-right hidden sm:block">
|
||||
<p className="text-xs text-muted-foreground">Vol</p>
|
||||
<p className="text-xs">{formatLargeNumber(token.volume24h)}</p>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddToWatchlist?.(token);
|
||||
}}
|
||||
>
|
||||
<Star className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -9,12 +9,19 @@ export { TokenAnalysisWidget, type TokenAnalysisWidgetProps, type TokenAnalysisD
|
|||
|
||||
// Epic 2: Smart Monitoring & Alerts
|
||||
export { WhaleActivityWidget, type WhaleActivityWidgetProps } from "./WhaleActivityWidget";
|
||||
export { TrendingTokensWidget, type TrendingTokensWidgetProps, type TrendingToken } from "./TrendingTokensWidget";
|
||||
|
||||
// Epic 3: Trading Intelligence
|
||||
export { TradingSuggestionWidget, type TradingSuggestionWidgetProps } from "./TradingSuggestionWidget";
|
||||
export { PortfolioWidget, type PortfolioWidgetProps } from "./PortfolioWidget";
|
||||
export { HolderAnalysisWidget, type HolderAnalysisWidgetProps, type HolderAnalysisData, type Holder } from "./HolderAnalysisWidget";
|
||||
|
||||
// Epic 4: Content Creation & Productivity
|
||||
export { ChartCaptureWidget, type ChartCaptureWidgetProps } from "./ChartCaptureWidget";
|
||||
export { ThreadGeneratorWidget, type ThreadGeneratorWidgetProps } from "./ThreadGeneratorWidget";
|
||||
|
||||
// Market Data Widgets
|
||||
export { MarketOverviewWidget, type MarketOverviewWidgetProps, type MarketOverviewData, type MarketToken } from "./MarketOverviewWidget";
|
||||
export { LiveTokenPriceWidget, type LiveTokenPriceWidgetProps, type LiveTokenPriceData } from "./LiveTokenPriceWidget";
|
||||
export { LiveTokenDataWidget, type LiveTokenDataWidgetProps, type LiveTokenDataInfo } from "./LiveTokenDataWidget";
|
||||
|
||||
|
|
|
|||
|
|
@ -128,3 +128,12 @@ export {
|
|||
type LiveTokenDataArgs,
|
||||
type LiveTokenDataResult,
|
||||
} from "./live-token-data";
|
||||
|
||||
// Trading Suggestion - displays AI-powered entry/exit suggestions
|
||||
export {
|
||||
TradingSuggestionToolUI,
|
||||
TradingSuggestionArgsSchema,
|
||||
TradingSuggestionResultSchema,
|
||||
type TradingSuggestionArgs,
|
||||
type TradingSuggestionResult,
|
||||
} from "./trading-suggestion";
|
||||
|
|
|
|||
236
surfsense_web/components/tool-ui/crypto/trading-suggestion.tsx
Normal file
236
surfsense_web/components/tool-ui/crypto/trading-suggestion.tsx
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
"use client";
|
||||
|
||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import { z } from "zod";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Target, AlertCircle, Info, TrendingUp, TrendingDown, 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 { useState } from "react";
|
||||
|
||||
// Schema for trading suggestion tool arguments
|
||||
export const TradingSuggestionArgsSchema = z.object({
|
||||
tokenSymbol: z.string(),
|
||||
tokenName: z.string().optional(),
|
||||
chain: z.string(),
|
||||
contractAddress: z.string().optional(),
|
||||
currentPrice: z.number(),
|
||||
entry: z.object({
|
||||
min: z.number(),
|
||||
max: z.number(),
|
||||
reasoning: z.string(),
|
||||
}),
|
||||
targets: z.array(z.object({
|
||||
level: z.number(),
|
||||
price: z.number(),
|
||||
percentGain: z.number(),
|
||||
confidence: z.number(),
|
||||
})),
|
||||
stopLoss: z.object({
|
||||
price: z.number(),
|
||||
percentLoss: z.number(),
|
||||
reasoning: z.string(),
|
||||
}),
|
||||
riskReward: z.number(),
|
||||
overallConfidence: z.number(),
|
||||
reasoning: z.array(z.string()),
|
||||
invalidationConditions: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type TradingSuggestionArgs = z.infer<typeof TradingSuggestionArgsSchema>;
|
||||
|
||||
// Schema for trading suggestion result
|
||||
export const TradingSuggestionResultSchema = z.object({
|
||||
success: z.boolean(),
|
||||
message: z.string().optional(),
|
||||
alertsSet: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type TradingSuggestionResult = z.infer<typeof TradingSuggestionResultSchema>;
|
||||
|
||||
const formatPrice = (price: number): string => {
|
||||
if (price < 0.00001) return `$${price.toExponential(2)}`;
|
||||
if (price < 0.01) return `$${price.toFixed(8)}`;
|
||||
if (price < 1) return `$${price.toFixed(6)}`;
|
||||
return `$${price.toFixed(4)}`;
|
||||
};
|
||||
|
||||
const getRiskRewardColor = (ratio: number) => {
|
||||
if (ratio >= 3) return "text-green-500";
|
||||
if (ratio >= 2) return "text-yellow-500";
|
||||
return "text-red-500";
|
||||
};
|
||||
|
||||
const getRiskRewardLabel = (ratio: number) => {
|
||||
if (ratio >= 3) return "Excellent";
|
||||
if (ratio >= 2) return "Good";
|
||||
if (ratio >= 1.5) return "Fair";
|
||||
return "Poor";
|
||||
};
|
||||
|
||||
/**
|
||||
* TradingSuggestionToolUI - Displays AI-powered trading suggestions in chat
|
||||
* Used when AI responds to queries like "suggest entry for BONK" or "trading suggestion for SOL"
|
||||
*/
|
||||
export const TradingSuggestionToolUI = makeAssistantToolUI<TradingSuggestionArgs, TradingSuggestionResult>({
|
||||
toolName: "trading_suggestion",
|
||||
render: ({ args, result, status }) => {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const isLoading = status.type === "running";
|
||||
|
||||
const handleOpenDexScreener = () => {
|
||||
if (args.contractAddress) {
|
||||
window.open(`https://dexscreener.com/${args.chain}/${args.contractAddress}`, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="my-3 overflow-hidden">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Target className="h-5 w-5 text-primary" />
|
||||
Trading Suggestion
|
||||
{isLoading && <Badge variant="secondary" className="animate-pulse">Analyzing...</Badge>}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xs text-muted-foreground">Confidence</div>
|
||||
<div className="font-bold text-sm">{args.overallConfidence}%</div>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Token Header */}
|
||||
<div className="flex items-center gap-3">
|
||||
<ChainIcon chain={args.chain} size="md" />
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-lg">{args.tokenSymbol}</span>
|
||||
{args.tokenName && <span className="text-muted-foreground text-sm">{args.tokenName}</span>}
|
||||
</div>
|
||||
<span className="font-medium text-xl">{formatPrice(args.currentPrice)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Entry Zone */}
|
||||
<div className="p-3 bg-green-500/10 border border-green-500/20 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-3 h-3 rounded-full bg-green-500" />
|
||||
<span className="font-semibold text-sm">Entry Zone</span>
|
||||
</div>
|
||||
<div className="font-bold text-lg text-green-600 dark:text-green-400 mb-1">
|
||||
{formatPrice(args.entry.min)} - {formatPrice(args.entry.max)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{args.entry.reasoning}</p>
|
||||
</div>
|
||||
|
||||
{/* Targets */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Target className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="font-semibold text-sm">Take Profit Targets</span>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
{args.targets.map((target) => (
|
||||
<div key={target.level} className="p-2 bg-blue-500/10 border border-blue-500/20 rounded-lg flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">🎯 T{target.level}</span>
|
||||
<span className="font-bold text-blue-600 dark:text-blue-400">{formatPrice(target.price)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-green-500 font-medium">+{target.percentGain.toFixed(1)}%</span>
|
||||
<Badge variant="outline" className="text-xs">{target.confidence}%</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stop Loss */}
|
||||
<div className="p-3 bg-red-500/10 border border-red-500/20 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500" />
|
||||
<span className="font-semibold text-sm">Stop Loss</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="font-bold text-lg text-red-600 dark:text-red-400">
|
||||
{formatPrice(args.stopLoss.price)}
|
||||
</span>
|
||||
<span className="text-sm text-red-500 font-medium">
|
||||
{args.stopLoss.percentLoss.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{args.stopLoss.reasoning}</p>
|
||||
</div>
|
||||
|
||||
{/* Risk/Reward */}
|
||||
<div className="p-3 bg-muted/50 rounded-lg flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Risk/Reward Ratio</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={args.riskReward >= 3 ? "default" : args.riskReward >= 2 ? "secondary" : "destructive"}>
|
||||
{getRiskRewardLabel(args.riskReward)}
|
||||
</Badge>
|
||||
<span className={cn("font-bold text-lg", getRiskRewardColor(args.riskReward))}>
|
||||
1:{args.riskReward.toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Why? Section - Collapsible */}
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
className="flex items-center gap-2 w-full text-left"
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
>
|
||||
<Info className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="font-semibold text-sm">Why?</span>
|
||||
<span className={cn("ml-auto transition-transform text-xs", showDetails && "rotate-180")}>▼</span>
|
||||
</button>
|
||||
|
||||
{showDetails && (
|
||||
<div className="space-y-3 pl-6 text-sm">
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-muted-foreground mb-1">Reasoning:</h4>
|
||||
<ul className="space-y-1">
|
||||
{args.reasoning.map((reason, i) => (
|
||||
<li key={i} className="text-xs flex items-start gap-2">
|
||||
<TrendingUp className="h-3 w-3 text-green-500 mt-0.5 flex-shrink-0" />
|
||||
<span>{reason}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-muted-foreground mb-1">Invalidation:</h4>
|
||||
<ul className="space-y-1">
|
||||
{args.invalidationConditions.map((condition, i) => (
|
||||
<li key={i} className="text-xs flex items-start gap-2">
|
||||
<AlertCircle className="h-3 w-3 text-red-500 mt-0.5 flex-shrink-0" />
|
||||
<span>{condition}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button variant="default" size="sm" className="flex-1">
|
||||
<Bell className="h-4 w-4 mr-2" />
|
||||
Set Alerts
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleOpenDexScreener}>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue