diff --git a/app/(root)/stocks/[symbol]/page.tsx b/app/(root)/stocks/[symbol]/page.tsx index 3b46ff8..4956ac2 100644 --- a/app/(root)/stocks/[symbol]/page.tsx +++ b/app/(root)/stocks/[symbol]/page.tsx @@ -14,7 +14,7 @@ import { import { auth } from '@/lib/better-auth/auth'; import { headers } from 'next/headers'; import { isStockInWatchlist } from '@/lib/actions/watchlist.actions'; -import { getStockDetailInsights } from '@/lib/actions/stock-analysis.actions'; +import { getStockSentimentInsights } from '@/lib/actions/adanos.actions'; import { formatSymbolForTradingView } from '@/lib/utils'; export default async function StockDetails({ params }: StockDetailsPageProps) { @@ -26,11 +26,10 @@ export default async function StockDetails({ params }: StockDetailsPageProps) { headers: await headers() }); const userId = session?.user?.id; - const [isInWatchlist, stockInsights] = await Promise.all([ + const [isInWatchlist, sentimentInsights] = await Promise.all([ userId ? isStockInWatchlist(userId, symbol) : Promise.resolve(false), - getStockDetailInsights(symbol), + getStockSentimentInsights(symbol), ]); - const { sentimentInsights, aiAnalysis } = stockInsights; return (
@@ -65,13 +64,16 @@ export default async function StockDetails({ params }: StockDetailsPageProps) {
- + diff --git a/components/stocks/StockAIAnalysisCard.tsx b/components/stocks/StockAIAnalysisCard.tsx index 0f9d01b..57cbfd1 100644 --- a/components/stocks/StockAIAnalysisCard.tsx +++ b/components/stocks/StockAIAnalysisCard.tsx @@ -1,7 +1,12 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { getAIAnalysis } from '@/lib/actions/stock-analysis.actions'; import type { StockAIAnalysis } from '@/lib/actions/stock-analysis.helpers'; interface StockAIAnalysisCardProps { - analysis: StockAIAnalysis | null; + symbol: string; + companyName?: string | null; } function getStanceClasses(stance: StockAIAnalysis['stance']) { @@ -30,17 +35,88 @@ function renderList(title: string, items: string[]) { ); } -export default function StockAIAnalysisCard({ analysis }: StockAIAnalysisCardProps) { +function LoadingSkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {[0, 1, 2].map((i) => ( +
+
+
+
+
+
+
+ ))} +
+
+
+
+ ); +} + +function ErrorState() { + return ( +
+

AI Stock Analysis

+

Professional research note unavailable

+

+ The stock dashboard data loaded, but an AI research note could not be generated right now. +

+
+ ); +} + +export default function StockAIAnalysisCard({ symbol, companyName }: StockAIAnalysisCardProps) { + const [analysis, setAnalysis] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + + async function fetchAnalysis() { + try { + const result = await getAIAnalysis(symbol, companyName); + if (!cancelled) { + setAnalysis(result); + } + } catch { + if (!cancelled) { + setAnalysis(null); + } + } finally { + if (!cancelled) { + setIsLoading(false); + } + } + } + + fetchAnalysis(); + + return () => { + cancelled = true; + }; + }, [symbol, companyName]); + + if (isLoading) { + return ; + } + if (!analysis) { - return ( -
-

AI Stock Analysis

-

Professional research note unavailable

-

- The stock dashboard data loaded, but an AI research note could not be generated right now. -

-
- ); + return ; } return ( diff --git a/diagnostic/image.png b/diagnostic/image.png new file mode 100644 index 0000000..85b0c7a Binary files /dev/null and b/diagnostic/image.png differ diff --git a/lib/actions/stock-analysis.actions.ts b/lib/actions/stock-analysis.actions.ts index 2439b02..f234845 100644 --- a/lib/actions/stock-analysis.actions.ts +++ b/lib/actions/stock-analysis.actions.ts @@ -102,3 +102,62 @@ export async function getStockDetailInsights(symbol: string): Promise { + const normalizedSymbol = symbol.trim().toUpperCase(); + + if (!normalizedSymbol || !hasConfiguredAIProvider()) { + return null; + } + + return unstable_cache( + async () => { + const [quote, profile, basicFinancials, news] = await Promise.all([ + getQuote(normalizedSymbol), + getCompanyProfile(normalizedSymbol), + getBasicFinancials(normalizedSymbol), + getCompanyNews(normalizedSymbol), + ]); + + const hasAnalysisContext = + Boolean(quote) || + Boolean(profile) || + Boolean(basicFinancials?.metric && Object.keys(basicFinancials.metric).length > 0) || + news.length > 0; + + if (!hasAnalysisContext) { + return null; + } + + try { + const response = await callAIProviderWithFallback( + buildStockAnalysisPrompt({ + symbol: normalizedSymbol, + quote, + profile, + basicFinancials, + sentimentInsights: null, + recentHeadlines: news.slice(0, 4).map(normalizeHeadline), + }), + ); + + return parseStockAnalysisResponse( + response, + normalizedSymbol, + companyName ?? profile?.name ?? null, + ); + } catch (error) { + console.error(`Failed to generate AI stock analysis for ${normalizedSymbol}`, error); + return null; + } + }, + ['ai-stock-analysis', normalizedSymbol], + { + revalidate: 900, + tags: [`stock-analysis:${normalizedSymbol}`], + }, + )(); +}