This commit is contained in:
MarkLo 2025-11-20 21:56:47 +08:00
parent eeb3d44423
commit 2872f18b47
62 changed files with 10750 additions and 0 deletions

29
.dockerignore Normal file
View File

@ -0,0 +1,29 @@
# Ignore git files
.git
.gitignore
# Ignore backend files from root
backend/
frontend/
# Ignore documentation
README.md
*.md
# Ignore results and cache
results/
__pycache__/
*.pyc
*.pyo
*.pyd
.pytest_cache/
.coverage
# Ignore development files
.vscode/
.idea/
*.swp
*.swo
# Ignore Mac files
.DS_Store

147
DOCKER_SETUP.md Normal file
View File

@ -0,0 +1,147 @@
# TradingAgents - Docker Setup Guide
This directory contains the Docker setup for running TradingAgents with a FastAPI backend and Next.js frontend.
## 🚀 Quick Start
### Prerequisites
- Docker and Docker Compose installed
- OpenAI API Key
- Alpha Vantage API Key (optional, for enhanced data)
### Environment Setup
1. Copy your `.env` file in the project root with your API keys:
```bash
OPENAI_API_KEY=your_openai_api_key_here
ALPHA_VANTAGE_API_KEY=your_alpha_vantage_api_key_here
```
### Running with Docker
1. **Build and start all services:**
```bash
docker-compose up --build
```
2. **Access the application:**
- Frontend: http://localhost:3000
- Backend API: http://localhost:8000
- API Documentation: http://localhost:8000/docs
3. **Stop the services:**
```bash
docker-compose down
```
## 📁 Project Structure
```
TradingAgents/
├── backend/ # FastAPI backend
│ ├── app/
│ │ ├── api/ # API routes
│ │ ├── models/ # Pydantic models
│ │ ├── services/ # Business logic
│ │ └── core/ # Configuration
│ ├── requirements.txt
│ └── Dockerfile
├── frontend/ # Next.js frontend
│ ├── app/ # Next.js pages
│ ├── components/ # React components
│ ├── lib/ # Utilities and API client
│ ├── hooks/ # Custom React hooks
│ └── Dockerfile
├── tradingagents/ # Core TradingAgents package
└── docker-compose.yml # Docker orchestration
```
## 🔧 Backend API
### Available Endpoints
- `GET /api/health` - Health check
- `GET /api/config` - Get configuration options
- `POST /api/analyze` - Run trading analysis
- `GET /api/tickers` - Get popular tickers
### Example API Request
```bash
curl -X POST http://localhost:8000/api/analyze \
-H "Content-Type: application/json" \
-d '{
"ticker": "NVDA",
"analysis_date": "2024-05-10",
"research_depth": 1
}'
```
## 🎨 Frontend Features
- **Modern UI**: Built with Next.js, React, and shadcn/ui
- **Type Safety**: Full TypeScript support
- **Responsive Design**: Works on desktop and mobile
- **Real-time Updates**: Live analysis status
- **Rich Visualizations**: Interactive reports and decisions
## 🛠️ Development
### Running Backend Only
```bash
cd backend
pip install -r requirements.txt
uvicorn app.main:app --reload
```
### Running Frontend Only
```bash
cd frontend
npm install
npm run dev
```
## 📊 Using the Application
1. Navigate to http://localhost:3000
2. Click "Start Analysis" or go to the Analysis page
3. Configure your analysis parameters:
- Stock ticker (e.g., NVDA, AAPL)
- Analysis date
- Research depth (1-5)
- LLM models
4. Click "Run Analysis"
5. Wait for the multi-agent analysis to complete
6. Review the trading decision and detailed reports
## 🔐 Security Notes
- Never commit `.env` files with real API keys
- Use environment variables for sensitive data
- The backend validates all inputs using Pydantic
- CORS is configured for frontend-backend communication
## 🐛 Troubleshooting
### Backend Issues
- Check logs: `docker-compose logs backend`
- Verify API keys are set correctly
- Ensure port 8000 is not in use
### Frontend Issues
- Check logs: `docker-compose logs frontend`
- Verify backend is running
- Ensure port 3000 is not in use
### Network Issues
- Restart Docker: `docker-compose down && docker-compose up`
- Check network: `docker network ls`
- Verify services can communicate: `docker-compose exec frontend ping backend`
## 📝 License
This project follows the same license as TradingAgents.
## 🤝 Contributing
See the main README.md for contribution guidelines.

26
backend/.dockerignore Normal file
View File

@ -0,0 +1,26 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
dist/
build/
# Virtual environments
venv/
env/
ENV/
# IDE
.vscode/
.idea/
*.swp
# Misc
.DS_Store
*.log
# Results
results/

41
backend/Dockerfile Normal file
View File

@ -0,0 +1,41 @@
# Multi-stage build for FastAPI backend
FROM python:3.11-slim as builder
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# Production stage
FROM python:3.11-slim
WORKDIR /app
# Copy dependencies from builder
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
# Copy application code
COPY ./app ./app
# Copy tradingagents package from parent directory
COPY ../tradingagents ./tradingagents
# Create results directory
RUN mkdir -p /app/results
# Expose port
EXPOSE 8000
# Run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

1
backend/app/__init__.py Normal file
View File

@ -0,0 +1 @@
"""App module initialization"""

View File

@ -0,0 +1 @@
"""API module initialization"""

View File

@ -0,0 +1,10 @@
"""
Shared dependencies for API routes
"""
from fastapi import Depends
from app.services.trading_service import TradingService, trading_service
def get_trading_service() -> TradingService:
"""Dependency to get trading service instance"""
return trading_service

101
backend/app/api/routes.py Normal file
View File

@ -0,0 +1,101 @@
"""
API route definitions for TradingAgents Backend
"""
from fastapi import APIRouter, Depends, HTTPException
from datetime import datetime
import logging
from app.models.schemas import (
AnalysisRequest,
AnalysisResponse,
ConfigResponse,
HealthResponse,
)
from app.services.trading_service import TradingService
from app.api.dependencies import get_trading_service
from app.core.config import settings
logger = logging.getLogger(__name__)
# Create API router
router = APIRouter(prefix="/api", tags=["TradingAgents"])
@router.get("/health", response_model=HealthResponse)
async def health_check():
"""Health check endpoint"""
return HealthResponse(
status="healthy",
version=settings.app_version,
timestamp=datetime.now().isoformat(),
)
@router.get("/config", response_model=ConfigResponse)
async def get_config(service: TradingService = Depends(get_trading_service)):
"""Get available configuration options"""
return ConfigResponse(
available_analysts=service.get_available_analysts(),
available_llms=service.get_available_llms(),
default_config=service.get_default_config(),
)
@router.post("/analyze", response_model=AnalysisResponse)
async def run_analysis(
request: AnalysisRequest,
service: TradingService = Depends(get_trading_service),
):
"""
Run trading analysis for a given ticker and date
This endpoint initiates a comprehensive trading analysis using the TradingAgents
multi-agent system. The analysis includes:
- Market technical analysis
- Sentiment analysis
- News analysis
- Fundamental analysis
- Research team debate
- Trading decision
- Risk assessment
- Portfolio management decision
The process may take several minutes depending on the research depth.
"""
logger.info(f"Received analysis request for {request.ticker} on {request.analysis_date}")
try:
# Run analysis
result = await service.run_analysis(
ticker=request.ticker,
analysis_date=request.analysis_date,
analysts=request.analysts,
research_depth=request.research_depth,
deep_think_llm=request.deep_think_llm,
quick_think_llm=request.quick_think_llm,
)
# Return response
return AnalysisResponse(**result)
except Exception as e:
logger.error(f"Analysis failed: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}")
@router.get("/tickers")
async def get_tickers():
"""Get list of popular tickers (example endpoint)"""
return {
"tickers": [
{"symbol": "AAPL", "name": "Apple Inc."},
{"symbol": "NVDA", "name": "NVIDIA Corporation"},
{"symbol": "MSFT", "name": "Microsoft Corporation"},
{"symbol": "GOOGL", "name": "Alphabet Inc."},
{"symbol": "AMZN", "name": "Amazon.com Inc."},
{"symbol": "TSLA", "name": "Tesla Inc."},
{"symbol": "META", "name": "Meta Platforms Inc."},
{"symbol": "SPY", "name": "SPDR S&P 500 ETF Trust"},
{"symbol": "QQQ", "name": "Invesco QQQ Trust"},
]
}

View File

@ -0,0 +1 @@
"""Core module initialization"""

View File

@ -0,0 +1,40 @@
"""
Configuration management for TradingAgents Backend API
"""
from pydantic_settings import BaseSettings
from typing import Optional
import os
class Settings(BaseSettings):
"""Application settings loaded from environment variables"""
# API Configuration
app_name: str = "TradingAgents API"
app_version: str = "1.0.0"
debug: bool = True
# API Keys
openai_api_key: Optional[str] = None
alpha_vantage_api_key: Optional[str] = None
# CORS Configuration
cors_origins: list = [
"http://localhost:3000",
"http://frontend:3000",
]
# TradingAgents Configuration
results_dir: str = "./results"
max_debate_rounds: int = 1
max_risk_discuss_rounds: int = 1
deep_think_llm: str = "gpt-4o-mini"
quick_think_llm: str = "gpt-4o-mini"
class Config:
env_file = ".env"
case_sensitive = False
# Global settings instance
settings = Settings()

16
backend/app/core/cors.py Normal file
View File

@ -0,0 +1,16 @@
"""
CORS configuration for TradingAgents Backend API
"""
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import settings
def setup_cors(app):
"""Configure CORS middleware for the FastAPI application"""
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

70
backend/app/main.py Normal file
View File

@ -0,0 +1,70 @@
"""
FastAPI application entry point for TradingAgents Backend
"""
from fastapi import FastAPI
from fastapi.responses import JSONResponse
import logging
import sys
from pathlib import Path
from app.core.config import settings
from app.core.cors import setup_cors
from app.api.routes import router
# Configure logging
logging.basicConfig(
level=logging.INFO if settings.debug else logging.WARNING,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
# Create FastAPI application
app = FastAPI(
title=settings.app_name,
version=settings.app_version,
description="Multi-Agent LLM Financial Trading Framework - REST API",
docs_url="/docs",
redoc_url="/redoc",
)
# Setup CORS
setup_cors(app)
# Include API routes
app.include_router(router)
@app.get("/")
async def root():
"""Root endpoint"""
return {
"message": "Welcome to TradingAgents API",
"version": settings.app_version,
"docs": "/docs",
"health": "/api/health",
}
@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
"""Global exception handler"""
logger.error(f"Unhandled exception: {str(exc)}", exc_info=True)
return JSONResponse(
status_code=500,
content={
"error": "Internal server error",
"detail": str(exc) if settings.debug else "An unexpected error occurred",
},
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=8000,
reload=settings.debug,
log_level="info" if settings.debug else "warning",
)

View File

@ -0,0 +1 @@
"""Models module initialization"""

View File

@ -0,0 +1,50 @@
"""
Pydantic models for request/response schemas
"""
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
from datetime import date
class AnalysisRequest(BaseModel):
"""Request model for trading analysis"""
ticker: str = Field(..., description="Stock ticker symbol (e.g., 'NVDA', 'AAPL')", min_length=1, max_length=10)
analysis_date: str = Field(..., description="Analysis date in YYYY-MM-DD format")
analysts: Optional[List[str]] = Field(
default=["market", "sentiment", "news", "fundamentals"],
description="List of analysts to include in analysis"
)
research_depth: Optional[int] = Field(default=1, ge=1, le=5, description="Research depth (1-5)")
deep_think_llm: Optional[str] = Field(default="gpt-4o-mini", description="Deep thinking LLM model")
quick_think_llm: Optional[str] = Field(default="gpt-4o-mini", description="Quick thinking LLM model")
class AnalysisResponse(BaseModel):
"""Response model for trading analysis"""
status: str = Field(..., description="Analysis status (success, error, processing)")
ticker: str = Field(..., description="Stock ticker analyzed")
analysis_date: str = Field(..., description="Date of analysis")
decision: Optional[Dict[str, Any]] = Field(None, description="Trading decision details")
reports: Optional[Dict[str, Any]] = Field(None, description="Analysis reports from different teams")
error: Optional[str] = Field(None, description="Error message if analysis failed")
class ConfigResponse(BaseModel):
"""Response model for configuration options"""
available_analysts: List[str] = Field(..., description="Available analyst types")
available_llms: Dict[str, List[str]] = Field(..., description="Available LLM models by provider")
default_config: Dict[str, Any] = Field(..., description="Default configuration values")
class HealthResponse(BaseModel):
"""Response model for health check"""
status: str = Field(..., description="API health status")
version: str = Field(..., description="API version")
timestamp: str = Field(..., description="Current server timestamp")
class ErrorResponse(BaseModel):
"""Response model for errors"""
error: str = Field(..., description="Error message")
detail: Optional[str] = Field(None, description="Detailed error information")
status_code: int = Field(..., description="HTTP status code")

View File

@ -0,0 +1 @@
"""Services module initialization"""

View File

@ -0,0 +1,144 @@
"""
TradingAgents service integration
"""
import sys
import os
from pathlib import Path
from typing import Dict, Any, List, Optional
import logging
# Add parent directory to path to import tradingagents
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
from tradingagents.graph.trading_graph import TradingAgentsGraph
from tradingagents.default_config import DEFAULT_CONFIG
from app.core.config import settings
logger = logging.getLogger(__name__)
class TradingService:
"""Service class for interacting with TradingAgents"""
def __init__(self):
self.default_config = DEFAULT_CONFIG.copy()
def create_config(
self,
research_depth: int = 1,
deep_think_llm: str = "gpt-4o-mini",
quick_think_llm: str = "gpt-4o-mini",
) -> Dict[str, Any]:
"""Create configuration for TradingAgents"""
config = self.default_config.copy()
config["max_debate_rounds"] = research_depth
config["max_risk_discuss_rounds"] = research_depth
config["deep_think_llm"] = deep_think_llm
config["quick_think_llm"] = quick_think_llm
config["results_dir"] = settings.results_dir
return config
async def run_analysis(
self,
ticker: str,
analysis_date: str,
analysts: Optional[List[str]] = None,
research_depth: int = 1,
deep_think_llm: str = "gpt-4o-mini",
quick_think_llm: str = "gpt-4o-mini",
) -> Dict[str, Any]:
"""
Run trading analysis for a given ticker and date
Args:
ticker: Stock ticker symbol
analysis_date: Date in YYYY-MM-DD format
analysts: List of analyst types to include
research_depth: Research depth (1-5)
deep_think_llm: Deep thinking LLM model
quick_think_llm: Quick thinking LLM model
Returns:
Dict containing analysis results
"""
try:
# Default analysts if not provided
if analysts is None:
analysts = ["market", "sentiment", "news", "fundamentals"]
# Create configuration
config = self.create_config(research_depth, deep_think_llm, quick_think_llm)
# Initialize TradingAgents graph
logger.info(f"Initializing TradingAgents for {ticker} on {analysis_date}")
graph = TradingAgentsGraph(analysts, config=config, debug=True)
# Run analysis
logger.info(f"Running analysis for {ticker}")
final_state, decision = graph.propagate(ticker, analysis_date)
# Extract reports from final state
reports = {
"market_report": final_state.get("market_report"),
"sentiment_report": final_state.get("sentiment_report"),
"news_report": final_state.get("news_report"),
"fundamentals_report": final_state.get("fundamentals_report"),
"investment_plan": final_state.get("investment_plan"),
"trader_investment_plan": final_state.get("trader_investment_plan"),
"final_trade_decision": final_state.get("final_trade_decision"),
"investment_debate_state": final_state.get("investment_debate_state"),
"risk_debate_state": final_state.get("risk_debate_state"),
}
return {
"status": "success",
"ticker": ticker,
"analysis_date": analysis_date,
"decision": decision,
"reports": reports,
}
except Exception as e:
logger.error(f"Analysis failed for {ticker}: {str(e)}", exc_info=True)
return {
"status": "error",
"ticker": ticker,
"analysis_date": analysis_date,
"error": str(e),
}
def get_available_analysts(self) -> List[str]:
"""Get list of available analyst types"""
return ["market", "sentiment", "news", "fundamentals"]
def get_available_llms(self) -> Dict[str, List[str]]:
"""Get list of available LLM models by provider"""
return {
"openai": [
"gpt-4o",
"gpt-4o-mini",
"gpt-4-turbo",
"gpt-4",
"gpt-3.5-turbo",
],
"anthropic": [
"claude-3-5-sonnet-20241022",
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-3-haiku-20240307",
],
}
def get_default_config(self) -> Dict[str, Any]:
"""Get default configuration"""
return {
"research_depth": 1,
"deep_think_llm": "gpt-4o-mini",
"quick_think_llm": "gpt-4o-mini",
"max_debate_rounds": 1,
"max_risk_discuss_rounds": 1,
}
# Global service instance
trading_service = TradingService()

38
backend/requirements.txt Normal file
View File

@ -0,0 +1,38 @@
# Backend requirements for FastAPI TradingAgents API
# Core Framework
fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic==2.5.0
pydantic-settings==2.1.0
# CORS and middleware
python-multipart==0.0.6
# Environment and configuration
python-dotenv==1.0.0
# Existing TradingAgents dependencies
typing-extensions
langchain-openai
langchain-experimental
pandas
yfinance
praw
feedparser
stockstats
eodhd
langgraph
chromadb
setuptools
backtrader
akshare
tushare
finnhub-python
parsel
requests
tqdm
pytz
redis
langchain_anthropic
langchain-google-genai

42
docker-compose.yml Normal file
View File

@ -0,0 +1,42 @@
version: '3.8'
services:
backend:
build:
context: .
dockerfile: backend/Dockerfile
container_name: tradingagents-backend
ports:
- "8000:8000"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- ALPHA_VANTAGE_API_KEY=${ALPHA_VANTAGE_API_KEY}
- TRADINGAGENTS_RESULTS_DIR=/app/results
volumes:
- ./results:/app/results
- ./tradingagents:/app/tradingagents:ro
networks:
- tradingagents-network
restart: unless-stopped
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: tradingagents-frontend
ports:
- "3000:3000"
environment:
- NEXT_PUBLIC_API_URL=http://backend:8000
depends_on:
- backend
networks:
- tradingagents-network
restart: unless-stopped
networks:
tradingagents-network:
driver: bridge
volumes:
results:

25
frontend/.dockerignore Normal file
View File

@ -0,0 +1,25 @@
# Node modules
node_modules
npm-debug.log*
# Next.js
.next/
out/
# Environment files
.env*.local
# Build files
dist/
build/
# IDE
.vscode/
.idea/
# Testing
coverage/
# Misc
.DS_Store
*.log

41
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

51
frontend/Dockerfile Normal file
View File

@ -0,0 +1,51 @@
# Multi-stage build for Next.js frontend
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies
COPY package.json package-lock.json* ./
RUN npm ci
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Set environment variable for build
ENV NEXT_PUBLIC_API_URL=http://backend:8000
# Build Next.js app
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set permissions for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

36
frontend/README.md Normal file
View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@ -0,0 +1,58 @@
/**
* Analysis page
*/
"use client";
import { useState } from "react";
import { AnalysisForm } from "@/components/analysis/AnalysisForm";
import { TradingDecision } from "@/components/analysis/TradingDecision";
import { AnalystReport } from "@/components/analysis/AnalystReport";
import { LoadingSpinner } from "@/components/shared/LoadingSpinner";
import { useAnalysis } from "@/hooks/useAnalysis";
import type { AnalysisRequest } from "@/lib/types";
export default function AnalysisPage() {
const { runAnalysis, loading, error, result } = useAnalysis();
const handleSubmit = async (request: AnalysisRequest) => {
try {
await runAnalysis(request);
} catch (err) {
// Error is handled by the hook
console.error("Analysis failed:", err);
}
};
return (
<div className="container mx-auto px-4 py-12">
<div className="max-w-6xl mx-auto space-y-8">
<div>
<h1 className="text-4xl font-bold mb-2"></h1>
<p className="text-gray-600 dark:text-gray-400">
</p>
</div>
<AnalysisForm onSubmit={handleSubmit} loading={loading} />
{loading && (
<LoadingSpinner message="正在執行分析... 這可能需要幾分鐘時間。" />
)}
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<h3 className="text-red-800 dark:text-red-300 font-semibold mb-2"></h3>
<p className="text-red-600 dark:text-red-400">{error}</p>
</div>
)}
{result && !loading && (
<div className="space-y-8">
<TradingDecision result={result} />
{result.reports && <AnalystReport reports={result.reports} />}
</div>
)}
</div>
</div>
);
}

BIN
frontend/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

122
frontend/app/globals.css Normal file
View File

@ -0,0 +1,122 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

32
frontend/app/layout.tsx Normal file
View File

@ -0,0 +1,32 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { Header } from "@/components/layout/Header";
import { Footer } from "@/components/layout/Footer";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "TradingAgents - 多代理 LLM 金融交易",
description: "由 AI 驅動的多代理 LLM 金融交易框架",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-1">
{children}
</main>
<Footer />
</div>
</body>
</html>
);
}

126
frontend/app/page.tsx Normal file
View File

@ -0,0 +1,126 @@
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
export default function HomePage() {
return (
<div className="container mx-auto px-4 py-12">
{/* Hero Section */}
<div className="text-center mb-16">
<h1 className="text-5xl font-bold mb-4 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
TradingAgents
</h1>
<p className="text-xl text-gray-600 dark:text-gray-400 mb-8 max-w-2xl mx-auto">
LLM
</p>
<div className="flex gap-4 justify-center">
<Link href="/analysis">
<Button size="lg" className="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700">
</Button>
</Link>
<a
href="https://github.com/TauricResearch/TradingAgents"
target="_blank"
rel="noopener noreferrer"
>
<Button size="lg" variant="outline">
GitHub
</Button>
</a>
</div>
</div>
{/* Features Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-16">
<FeatureCard
title="分析師團隊"
description="由 AI 代理驅動的市場、情緒、新聞與基本面分析"
icon="📊"
/>
<FeatureCard
title="研究團隊"
description="看漲與看跌研究員進行辯論以找出最佳策略"
icon="🔍"
/>
<FeatureCard
title="交易代理"
description="整合見解並做出明智的交易決策"
icon="💼"
/>
<FeatureCard
title="風險管理"
description="評估投資組合風險並調整策略"
icon="🛡️"
/>
</div>
{/* Workflow Section */}
<Card className="shadow-lg">
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>
TradingAgents LLM
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<WorkflowStep
number={1}
title="分析師團隊"
description="多位分析師評估市場狀況、情緒、新聞和基本面"
/>
<WorkflowStep
number={2}
title="研究團隊"
description="看漲和看跌研究員進行結構化辯論"
/>
<WorkflowStep
number={3}
title="交易員"
description="審查所有報告並確定交易行動"
/>
<WorkflowStep
number={4}
title="風險管理"
description="評估風險因素並提供建議"
/>
<WorkflowStep
number={5}
title="投資組合經理"
description="做出最終決定以批准或拒絕交易"
/>
</div>
</CardContent>
</Card>
</div>
);
}
function FeatureCard({ title, description, icon }: { title: string; description: string; icon: string }) {
return (
<Card className="hover:shadow-lg transition-shadow">
<CardHeader>
<div className="text-4xl mb-2">{icon}</div>
<CardTitle className="text-lg">{title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-gray-600 dark:text-gray-400">{description}</p>
</CardContent>
</Card>
);
}
function WorkflowStep({ number, title, description }: { number: number; title: string; description: string }) {
return (
<div className="flex gap-4 items-start">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-gradient-to-r from-blue-600 to-purple-600 text-white flex items-center justify-center font-bold">
{number}
</div>
<div>
<h4 className="font-semibold mb-1">{title}</h4>
<p className="text-sm text-gray-600 dark:text-gray-400">{description}</p>
</div>
</div>
);
}

22
frontend/components.json Normal file
View File

@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

View File

@ -0,0 +1,178 @@
/**
* Analysis form component
*/
"use client";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { format } from "date-fns";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import type { AnalysisRequest } from "@/lib/types";
const formSchema = z.object({
ticker: z.string().min(1, "股票代碼為必填").max(10),
analysis_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "日期格式必須為 YYYY-MM-DD"),
research_depth: z.number().min(1).max(5),
deep_think_llm: z.string(),
quick_think_llm: z.string(),
});
interface AnalysisFormProps {
onSubmit: (data: AnalysisRequest) => void;
loading?: boolean;
}
export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
ticker: "NVDA",
analysis_date: format(new Date(), "yyyy-MM-dd"),
research_depth: 1,
deep_think_llm: "gpt-4o-mini",
quick_think_llm: "gpt-4o-mini",
},
});
function handleSubmit(values: z.infer<typeof formSchema>) {
const request: AnalysisRequest = {
...values,
analysts: ["market", "sentiment", "news", "fundamentals"],
};
onSubmit(request);
}
return (
<Card className="w-full shadow-lg">
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<FormField
control={form.control}
name="ticker"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="NVDA" {...field} />
</FormControl>
<FormDescription>
NVDAAAPL
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="analysis_date"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input type="date" {...field} />
</FormControl>
<FormDescription>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="research_depth"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Select
onValueChange={(value) => field.onChange(parseInt(value))}
defaultValue={field.value.toString()}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="選擇深度" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="1">1 - </SelectItem>
<SelectItem value="2">2 - </SelectItem>
<SelectItem value="3">3 - </SelectItem>
<SelectItem value="4">4 - </SelectItem>
<SelectItem value="5">5 - </SelectItem>
</SelectContent>
</Select>
<FormDescription>
=
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="deep_think_llm"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="選擇模型" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="gpt-4o">GPT-4o</SelectItem>
<SelectItem value="gpt-4o-mini">GPT-4o Mini</SelectItem>
<SelectItem value="gpt-4-turbo">GPT-4 Turbo</SelectItem>
</SelectContent>
</Select>
<FormDescription>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<Button type="submit" className="w-full" disabled={loading} size="lg">
{loading ? "執行分析中..." : "執行分析"}
</Button>
</form>
</Form>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,133 @@
/**
* Analyst reports display component
*/
"use client";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import type { Reports } from "@/lib/types";
interface AnalystReportProps {
reports: Reports;
}
export function AnalystReport({ reports }: AnalystReportProps) {
const hasAnalystReports =
reports.market_report ||
reports.sentiment_report ||
reports.news_report ||
reports.fundamentals_report;
const hasResearchReports =
reports.investment_debate_state?.bull_history ||
reports.investment_debate_state?.bear_history ||
reports.investment_debate_state?.judge_decision;
const hasRiskReports =
reports.risk_debate_state?.risky_history ||
reports.risk_debate_state?.safe_history ||
reports.risk_debate_state?.neutral_history;
if (!hasAnalystReports && !hasResearchReports && !hasRiskReports) {
return null;
}
return (
<Card className="shadow-lg">
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="analysts" className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="analysts"></TabsTrigger>
<TabsTrigger value="research"></TabsTrigger>
<TabsTrigger value="trader"></TabsTrigger>
<TabsTrigger value="risk"></TabsTrigger>
</TabsList>
<TabsContent value="analysts" className="space-y-4">
{reports.market_report && (
<ReportSection title="市場分析" content={reports.market_report} />
)}
{reports.sentiment_report && (
<ReportSection title="情緒分析" content={reports.sentiment_report} />
)}
{reports.news_report && (
<ReportSection title="新聞分析" content={reports.news_report} />
)}
{reports.fundamentals_report && (
<ReportSection title="基本面分析" content={reports.fundamentals_report} />
)}
</TabsContent>
<TabsContent value="research" className="space-y-4">
{reports.investment_debate_state?.bull_history && (
<ReportSection
title="看漲研究員"
content={reports.investment_debate_state.bull_history}
/>
)}
{reports.investment_debate_state?.bear_history && (
<ReportSection
title="看跌研究員"
content={reports.investment_debate_state.bear_history}
/>
)}
{reports.investment_debate_state?.judge_decision && (
<ReportSection
title="研究經理決策"
content={reports.investment_debate_state.judge_decision}
/>
)}
</TabsContent>
<TabsContent value="trader" className="space-y-4">
{reports.trader_investment_plan && (
<ReportSection title="交易員計劃" content={reports.trader_investment_plan} />
)}
</TabsContent>
<TabsContent value="risk" className="space-y-4">
{reports.risk_debate_state?.risky_history && (
<ReportSection
title="激進分析師"
content={reports.risk_debate_state.risky_history}
/>
)}
{reports.risk_debate_state?.safe_history && (
<ReportSection
title="保守分析師"
content={reports.risk_debate_state.safe_history}
/>
)}
{reports.risk_debate_state?.neutral_history && (
<ReportSection
title="中立分析師"
content={reports.risk_debate_state.neutral_history}
/>
)}
{reports.risk_debate_state?.judge_decision && (
<ReportSection
title="投資組合經理決策"
content={reports.risk_debate_state.judge_decision}
/>
)}
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
}
function ReportSection({ title, content }: { title: string; content: string }) {
return (
<div className="border rounded-lg p-4">
<h3 className="font-semibold text-lg mb-2">{title}</h3>
<div className="prose prose-sm dark:prose-invert max-w-none">
<p className="whitespace-pre-wrap text-sm">{content}</p>
</div>
</div>
);
}

View File

@ -0,0 +1,95 @@
/**
* Trading decision display component
*/
"use client";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import type { AnalysisResponse } from "@/lib/types";
interface TradingDecisionProps {
result: AnalysisResponse;
}
export function TradingDecision({ result }: TradingDecisionProps) {
if (result.status === "error") {
return (
<Card className="border-red-500">
<CardHeader>
<CardTitle className="text-red-600"></CardTitle>
</CardHeader>
<CardContent>
<p className="text-red-500">{result.error}</p>
</CardContent>
</Card>
);
}
if (!result.decision) {
return null;
}
const getActionBadge = (action: string) => {
const actionLower = action.toLowerCase();
if (actionLower.includes("buy")) {
return <Badge className="bg-green-600"></Badge>;
} else if (actionLower.includes("sell")) {
return <Badge className="bg-red-600"></Badge>;
} else {
return <Badge className="bg-yellow-600"></Badge>;
}
};
return (
<Card className="shadow-lg border-2 border-blue-500">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle></CardTitle>
{getActionBadge(result.decision.action || "")}
</div>
<CardDescription>
{result.ticker} {result.analysis_date}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h4 className="font-semibold mb-2"></h4>
<p className="text-lg">{result.decision.action}</p>
</div>
{result.decision.quantity && (
<div>
<h4 className="font-semibold mb-2"></h4>
<p>{result.decision.quantity} </p>
</div>
)}
{result.decision.confidence && (
<div>
<h4 className="font-semibold mb-2"></h4>
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-blue-600"
style={{ width: `${result.decision.confidence * 100}%` }}
/>
</div>
<span className="text-sm font-medium">
{(result.decision.confidence * 100).toFixed(0)}%
</span>
</div>
</div>
)}
{result.decision.reasoning && (
<div>
<h4 className="font-semibold mb-2"></h4>
<p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
{result.decision.reasoning}
</p>
</div>
)}
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,42 @@
/**
* Footer component
*/
export function Footer() {
return (
<footer className="border-t bg-gray-50 dark:bg-gray-900">
<div className="container mx-auto px-4 py-6">
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
<div className="text-sm text-gray-600 dark:text-gray-400">
© 2025 TradingAgents. {" "}
<a
href="https://github.com/TauricResearch"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
Tauric Research
</a>
</div>
<div className="flex gap-4 text-sm">
<a
href="https://github.com/TauricResearch/TradingAgents"
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400"
>
GitHub
</a>
<a
href="https://arxiv.org/abs/2412.20138"
target="_blank"
rel="noopener noreferrer"
className="text-gray-600 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400"
>
</a>
</div>
</div>
</div>
</footer>
);
}

View File

@ -0,0 +1,35 @@
/**
* Header component
*/
import Link from "next/link";
export function Header() {
return (
<header className="border-b bg-gradient-to-r from-blue-600 to-purple-600 text-white">
<div className="container mx-auto px-4 py-6">
<div className="flex items-center justify-between">
<Link href="/" className="flex items-center gap-2">
<div className="text-3xl font-bold">TradingAgents</div>
<div className="hidden md:block text-sm font-light opacity-90">
LLM
</div>
</Link>
<nav className="flex gap-6">
<Link
href="/"
className="hover:opacity-80 transition-opacity font-medium"
>
</Link>
<Link
href="/analysis"
className="hover:opacity-80 transition-opacity font-medium"
>
</Link>
</nav>
</div>
</div>
</header>
);
}

View File

@ -0,0 +1,17 @@
/**
* Loading spinner component
*/
import { Spinner } from "@/components/ui/spinner";
interface LoadingSpinnerProps {
message?: string;
}
export function LoadingSpinner({ message = "載入中..." }: LoadingSpinnerProps) {
return (
<div className="flex flex-col items-center justify-center py-12 gap-4">
<Spinner className="h-8 w-8" />
<p className="text-sm text-gray-600 dark:text-gray-400">{message}</p>
</div>
);
}

View File

@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,60 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@ -0,0 +1,143 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@ -0,0 +1,167 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
)
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
if (!body) {
return null
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
)
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

View File

@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View File

@ -0,0 +1,187 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@ -0,0 +1,16 @@
import { Loader2Icon } from "lucide-react"
import { cn } from "@/lib/utils"
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
return (
<Loader2Icon
role="status"
aria-label="Loading"
className={cn("size-4 animate-spin", className)}
{...props}
/>
)
}
export { Spinner }

View File

@ -0,0 +1,116 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

View File

@ -0,0 +1,47 @@
/**
* Custom hook for trading analysis
*/
"use client";
import { useState } from "react";
import { api } from "@/lib/api";
import type { AnalysisRequest, AnalysisResponse } from "@/lib/types";
export function useAnalysis() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<AnalysisResponse | null>(null);
const runAnalysis = async (request: AnalysisRequest) => {
setLoading(true);
setError(null);
setResult(null);
try {
const response = await api.runAnalysis(request);
setResult(response);
return response;
} catch (err: any) {
const errorMessage =
err.response?.data?.detail || err.message || "Analysis failed";
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
};
const reset = () => {
setLoading(false);
setError(null);
setResult(null);
};
return {
runAnalysis,
loading,
error,
result,
reset,
};
}

View File

@ -0,0 +1,31 @@
/**
* Custom hook for fetching configuration
*/
"use client";
import { useState, useEffect } from "react";
import { api } from "@/lib/api";
import type { ConfigResponse } from "@/lib/types";
export function useConfig() {
const [config, setConfig] = useState<ConfigResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchConfig = async () => {
try {
const response = await api.getConfig();
setConfig(response);
} catch (err: any) {
setError(err.message || "Failed to fetch configuration");
} finally {
setLoading(false);
}
};
fetchConfig();
}, []);
return { config, loading, error };
}

57
frontend/lib/api.ts Normal file
View File

@ -0,0 +1,57 @@
/**
* API client for TradingAgents backend
*/
import axios from "axios";
import type {
AnalysisRequest,
AnalysisResponse,
ConfigResponse,
HealthResponse,
Ticker,
} from "./types";
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
const apiClient = axios.create({
baseURL: API_URL,
headers: {
"Content-Type": "application/json",
},
});
export const api = {
/**
* Get API health status
*/
async health(): Promise<HealthResponse> {
const response = await apiClient.get<HealthResponse>("/api/health");
return response.data;
},
/**
* Get configuration options
*/
async getConfig(): Promise<ConfigResponse> {
const response = await apiClient.get<ConfigResponse>("/api/config");
return response.data;
},
/**
* Run trading analysis
*/
async runAnalysis(request: AnalysisRequest): Promise<AnalysisResponse> {
const response = await apiClient.post<AnalysisResponse>(
"/api/analyze",
request
);
return response.data;
},
/**
* Get list of popular tickers
*/
async getTickers(): Promise<{ tickers: Ticker[] }> {
const response = await apiClient.get<{ tickers: Ticker[] }>("/api/tickers");
return response.data;
},
};

70
frontend/lib/types.ts Normal file
View File

@ -0,0 +1,70 @@
/**
* TypeScript type definitions for TradingAgents API
*/
export interface AnalysisRequest {
ticker: string;
analysis_date: string;
analysts?: string[];
research_depth?: number;
deep_think_llm?: string;
quick_think_llm?: string;
}
export interface AnalysisResponse {
status: "success" | "error" | "processing";
ticker: string;
analysis_date: string;
decision?: Decision;
reports?: Reports;
error?: string;
}
export interface Decision {
action: string;
quantity?: number;
reasoning?: string;
confidence?: number;
}
export interface Reports {
market_report?: string;
sentiment_report?: string;
news_report?: string;
fundamentals_report?: string;
investment_plan?: string;
trader_investment_plan?: string;
final_trade_decision?: string;
investment_debate_state?: DebateState;
risk_debate_state?: DebateState;
}
export interface DebateState {
bull_history?: string;
bear_history?: string;
risky_history?: string;
safe_history?: string;
neutral_history?: string;
judge_decision?: string;
}
export interface ConfigResponse {
available_analysts: string[];
available_llms: {
[provider: string]: string[];
};
default_config: {
[key: string]: any;
};
}
export interface HealthResponse {
status: string;
version: string;
timestamp: string;
}
export interface Ticker {
symbol: string;
name: string;
}

6
frontend/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

5
frontend/next.config.js Normal file
View File

@ -0,0 +1,5 @@
const nextConfig = {
output: 'standalone',
};
export default nextConfig;

8
frontend/next.config.ts Normal file
View File

@ -0,0 +1,8 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
reactCompiler: true,
};
export default nextConfig;

7710
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
frontend/package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.554.0",
"next": "16.0.3",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-hook-form": "^7.66.1",
"tailwind-merge": "^3.4.0",
"zod": "^4.1.12"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"babel-plugin-react-compiler": "1.0.0",
"eslint": "^9",
"eslint-config-next": "16.0.3",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
}
}

View File

@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

1
frontend/public/file.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
frontend/public/next.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

34
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}