162 lines
6.6 KiB
TypeScript
162 lines
6.6 KiB
TypeScript
'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 {
|
|
symbol: string;
|
|
companyName?: string | null;
|
|
}
|
|
|
|
function getStanceClasses(stance: StockAIAnalysis['stance']) {
|
|
if (stance === 'Bullish') return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-300';
|
|
if (stance === 'Bearish') return 'border-rose-500/30 bg-rose-500/10 text-rose-300';
|
|
return 'border-amber-500/30 bg-amber-500/10 text-amber-200';
|
|
}
|
|
|
|
function renderList(title: string, items: string[]) {
|
|
return (
|
|
<div className="rounded-xl border border-gray-800 bg-black/20 p-4">
|
|
<h3 className="text-sm font-semibold uppercase tracking-[0.18em] text-gray-400">{title}</h3>
|
|
{items.length ? (
|
|
<ul className="mt-3 space-y-3">
|
|
{items.map((item) => (
|
|
<li key={item} className="flex gap-3 text-sm text-gray-200">
|
|
<span className="mt-2 h-1.5 w-1.5 rounded-full bg-yellow-400" />
|
|
<span>{item}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
) : (
|
|
<p className="mt-3 text-sm text-gray-400">No additional signals generated.</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function LoadingSkeleton() {
|
|
return (
|
|
<section className="rounded-2xl border border-gray-800 bg-gray-950/40 p-5 backdrop-blur-sm">
|
|
<div className="flex flex-col gap-4 animate-pulse">
|
|
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
|
<div className="flex-1">
|
|
<div className="h-3 w-32 rounded bg-gray-700" />
|
|
<div className="mt-2 h-6 w-48 rounded bg-gray-700" />
|
|
<div className="mt-3 h-4 w-full rounded bg-gray-700" />
|
|
<div className="mt-2 h-4 w-3/4 rounded bg-gray-700" />
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<div className="h-6 w-20 rounded-full bg-gray-700" />
|
|
<div className="h-6 w-28 rounded-full bg-gray-700" />
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
{[0, 1, 2].map((i) => (
|
|
<div key={i} className="rounded-xl border border-gray-800 bg-black/20 p-4">
|
|
<div className="h-3 w-20 rounded bg-gray-700" />
|
|
<div className="mt-3 space-y-3">
|
|
<div className="h-3 w-full rounded bg-gray-700" />
|
|
<div className="h-3 w-5/6 rounded bg-gray-700" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="h-3 w-72 rounded bg-gray-700" />
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function ErrorState() {
|
|
return (
|
|
<section className="rounded-2xl border border-gray-800 bg-gray-950/40 p-5 backdrop-blur-sm">
|
|
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-gray-500">AI Stock Analysis</p>
|
|
<h2 className="mt-2 text-xl font-semibold text-white">Professional research note unavailable</h2>
|
|
<p className="mt-3 text-sm leading-6 text-gray-400">
|
|
The stock dashboard data loaded, but an AI research note could not be generated right now.
|
|
</p>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export default function StockAIAnalysisCard({ symbol, companyName }: StockAIAnalysisCardProps) {
|
|
const [analysis, setAnalysis] = useState<StockAIAnalysis | null>(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 <LoadingSkeleton />;
|
|
}
|
|
|
|
if (!analysis) {
|
|
return <ErrorState />;
|
|
}
|
|
|
|
return (
|
|
<section className="rounded-2xl border border-gray-800 bg-gray-950/40 p-5 backdrop-blur-sm">
|
|
<div className="flex flex-col gap-4">
|
|
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
|
<div>
|
|
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-gray-500">
|
|
AI Stock Analysis
|
|
</p>
|
|
<h2 className="mt-2 text-xl font-semibold text-white">
|
|
{analysis.companyName ? `${analysis.companyName} (${analysis.symbol})` : analysis.symbol}
|
|
</h2>
|
|
<p className="mt-2 text-sm leading-6 text-gray-300">{analysis.summary}</p>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<span
|
|
className={`rounded-full border px-3 py-1 text-xs font-semibold uppercase tracking-[0.18em] ${getStanceClasses(analysis.stance)}`}
|
|
>
|
|
{analysis.stance}
|
|
</span>
|
|
<span className="rounded-full border border-gray-700 bg-black/20 px-3 py-1 text-xs font-semibold uppercase tracking-[0.18em] text-gray-200">
|
|
{analysis.confidence} confidence
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
{renderList('Key drivers', analysis.keyDrivers)}
|
|
{renderList('Risks', analysis.risks)}
|
|
{renderList('What to watch', analysis.watchItems)}
|
|
</div>
|
|
|
|
<p className="text-xs text-gray-500">
|
|
Educational analysis only. Use it as a synthesis of the dashboard inputs, not as personal
|
|
financial advice.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|