diff --git a/backend/app/services/trading_service.py b/backend/app/services/trading_service.py index ddac9806..a67cd480 100644 --- a/backend/app/services/trading_service.py +++ b/backend/app/services/trading_service.py @@ -182,11 +182,74 @@ class TradingService: except Exception as e: logger.error(f"Analysis failed for {ticker}: {str(e)}", exc_info=True) + + # Check if it's a rate limit error + error_message = str(e) + error_type = "general" + retry_after = None + quota_limit = None + quota_metric = None + + # Detect OpenAI/Gemini Rate Limit Errors + if "Error code: 429" in error_message or "RateLimitError" in str(type(e).__name__): + error_type = "rate_limit" + + # Extract quota details from error message + import re + + # Extract limit (e.g., "limit: 20") + limit_match = re.search(r'limit:\s*(\d+)', error_message) + if limit_match: + quota_limit = int(limit_match.group(1)) + + # Extract model name + model_match = re.search(r'model:\s*([\w\-\.]+)', error_message) + model_name = model_match.group(1) if model_match else "unknown" + + # Extract retry time (e.g., "retry in 37.312655565s" or "retryDelay": "37s") + retry_match = re.search(r'retry in ([\d\.]+)s', error_message) + if not retry_match: + retry_match = re.search(r'"retryDelay":\s*"(\d+)s"', error_message) + + if retry_match: + retry_after = int(float(retry_match.group(1))) + + # Extract quota metric name + metric_match = re.search(r'quotaMetric["\']:\s*["\']([^"\']+)', error_message) + if metric_match: + quota_metric = metric_match.group(1) + + # Create user-friendly message + if quota_limit and model_name: + error_message = ( + f"API Rate Limit Exceeded: You've reached the quota limit of {quota_limit} requests " + f"for model '{model_name}'. " + ) + if retry_after: + minutes = retry_after // 60 + seconds = retry_after % 60 + if minutes > 0: + error_message += f"Please retry in {minutes} minute(s) and {seconds} second(s). " + else: + error_message += f"Please retry in {seconds} second(s). " + error_message += ( + "Consider upgrading to a paid plan for higher limits, or reduce the number of " + "analysts/research depth to minimize API calls." + ) + else: + error_message = ( + "API Rate Limit Exceeded: You've exceeded your quota. " + "Please wait before retrying, or consider upgrading to a paid plan." + ) + return { "status": "error", "ticker": ticker, "analysis_date": analysis_date, - "error": str(e), + "error": error_message, + "error_type": error_type, + "retry_after": retry_after, + "quota_limit": quota_limit, } def get_available_analysts(self) -> List[str]: diff --git a/frontend/app/analysis/page.tsx b/frontend/app/analysis/page.tsx index e17fbbae..f3cfe0b4 100644 --- a/frontend/app/analysis/page.tsx +++ b/frontend/app/analysis/page.tsx @@ -7,6 +7,7 @@ import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { AnalysisForm } from "@/components/analysis/AnalysisForm"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; +import { ErrorAlert } from "@/components/shared/ErrorAlert"; import { useAnalysis } from "@/hooks/useAnalysis"; import { useAnalysisContext } from "@/context/AnalysisContext"; import type { AnalysisRequest } from "@/lib/types"; @@ -62,12 +63,7 @@ export default function AnalysisPage() { )} - {error && ( -
-

錯誤

-

{error}

-
- )} + {error && } diff --git a/frontend/components/shared/ErrorAlert.tsx b/frontend/components/shared/ErrorAlert.tsx new file mode 100644 index 00000000..34a88e8a --- /dev/null +++ b/frontend/components/shared/ErrorAlert.tsx @@ -0,0 +1,127 @@ +/** + * Error Alert Component + * Displays user-friendly error messages with special handling for rate limits + */ +"use client"; + +import { AlertCircle, Clock, TrendingUp } from "lucide-react"; +import { Card } from "@/components/ui/card"; + +interface ErrorAlertProps { + error: string | { + error: string; + error_type?: string; + retry_after?: number; + quota_limit?: number; + }; +} + +export function ErrorAlert({ error }: ErrorAlertProps) { + // Parse error data + const isRateLimit = typeof error === "object" && error.error_type === "rate_limit"; + const errorMessage = typeof error === "string" ? error : error.error; + const retryAfter = typeof error === "object" ? error.retry_after : null; + const quotaLimit = typeof error === "object" ? error.quota_limit : null; + + // Calculate retry time display + const getRetryTimeDisplay = (seconds: number | null) => { + if (!seconds) return null; + + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + + if (minutes > 0) { + return `${minutes} 分 ${remainingSeconds} 秒`; + } + return `${remainingSeconds} 秒`; + }; + + return ( + +
+
+ {isRateLimit ? ( + + ) : ( + + )} +
+ +
+

+ {isRateLimit ? "API 請求額度已達上限" : "錯誤"} +

+ +

+ {errorMessage} +

+ + {isRateLimit && ( +
+ {/* Retry Information */} + {retryAfter && ( +
+ +
+

+ 建議等待時間 +

+

+ 請在 + {getRetryTimeDisplay(retryAfter)} + 後重試 +

+
+
+ )} + + {/* Quota Information */} + {quotaLimit && ( +
+ +
+

+ 每日額度限制 +

+

+ 當前計劃:每日 {quotaLimit} 次請求 +

+
+
+ )} + + {/* Solutions */} +
+

+ 💡 解決方案: +

+
    +
  • 等待額度重置(通常為每日重置)
  • +
  • 升級至付費方案以獲得更高額度
  • +
  • 減少分析師數量或研究深度以降低 API 呼叫次數
  • +
  • 使用不同的 API 金鑰(如果有多個帳戶)
  • +
+
+
+ )} +
+
+
+ ); +} diff --git a/frontend/hooks/useAnalysis.ts b/frontend/hooks/useAnalysis.ts index c9860400..ea0a2acb 100644 --- a/frontend/hooks/useAnalysis.ts +++ b/frontend/hooks/useAnalysis.ts @@ -9,7 +9,12 @@ import type { AnalysisRequest, AnalysisResponse } from "@/lib/types"; export function useAnalysis() { const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const [result, setResult] = useState(null); const [taskId, setTaskId] = useState(null); const [progress, setProgress] = useState(null); @@ -42,7 +47,17 @@ export function useAnalysis() { // Check if failed if (status.status === "failed") { - setError(status.error || "Analysis failed"); + // Check if we have structured error data from result + if (status.result && status.result.error) { + setError({ + error: status.result.error, + error_type: status.result.error_type, + retry_after: status.result.retry_after, + quota_limit: status.result.quota_limit, + }); + } else { + setError(status.error || "Analysis failed"); + } setLoading(false); setProgress(null); // Stop polling diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index 92215ea8..4becec39 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -47,6 +47,9 @@ export interface AnalysisResponse { decision?: any; reports?: any; error?: string; + error_type?: string; + retry_after?: number; + quota_limit?: number; price_data?: PriceData[]; price_stats?: PriceStats; }