'use client'; import { useQueryClient } from '@tanstack/react-query'; import Link from 'next/link'; import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { format } from 'date-fns'; import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; import { BrainCircuit, ChartNoAxesCombined, NotebookTabs, NotebookPen, RefreshCcw, Search, SquarePen, Trash2 } from 'lucide-react'; import { useSearchParams } from 'next/navigation'; import { AppShell } from '@/components/shell/app-shell'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Panel } from '@/components/ui/panel'; import { useAuthGuard } from '@/hooks/use-auth-guard'; import { useLinkPrefetch } from '@/hooks/use-link-prefetch'; import { buildGraphingHref } from '@/lib/graphing/catalog'; import { createResearchJournalEntry, deleteResearchJournalEntry, updateResearchJournalEntry } from '@/lib/api'; import { asNumber, formatCurrency, formatCurrencyByScale, formatPercent, type NumberScaleUnit } from '@/lib/format'; import { queryKeys } from '@/lib/query/keys'; import { companyAnalysisQueryOptions, researchJournalQueryOptions } from '@/lib/query/options'; import type { CompanyAnalysis, ResearchJournalEntry } from '@/lib/types'; type FinancialPeriodFilter = 'quarterlyAndFiscalYearEnd' | 'quarterlyOnly' | 'fiscalYearEndOnly'; type FinancialSeriesPoint = { filingDate: string; filingType: '10-K' | '10-Q'; periodLabel: 'Quarter End' | 'Fiscal Year End'; revenue: number | null; netIncome: number | null; assets: number | null; netMargin: number | null; }; type JournalFormState = { title: string; bodyMarkdown: string; accessionNumber: string; }; const EMPTY_JOURNAL_FORM: JournalFormState = { title: '', bodyMarkdown: '', accessionNumber: '' }; const FINANCIAL_PERIOD_FILTER_OPTIONS: Array<{ value: FinancialPeriodFilter; label: string }> = [ { value: 'quarterlyAndFiscalYearEnd', label: 'Quarterly + Fiscal Year End' }, { value: 'quarterlyOnly', label: 'Quarterly only' }, { value: 'fiscalYearEndOnly', label: 'Fiscal Year End only' } ]; const FINANCIAL_VALUE_SCALE_OPTIONS: Array<{ value: NumberScaleUnit; label: string }> = [ { value: 'thousands', label: 'Thousands (K)' }, { value: 'millions', label: 'Millions (M)' }, { value: 'billions', label: 'Billions (B)' } ]; const CHART_TEXT = '#f3f5f7'; const CHART_MUTED = '#a1a9b3'; const CHART_GRID = 'rgba(196, 202, 211, 0.18)'; const CHART_TOOLTIP_BG = 'rgba(31, 34, 39, 0.96)'; const CHART_TOOLTIP_BORDER = 'rgba(220, 226, 234, 0.24)'; function formatShortDate(value: string) { return format(new Date(value), 'MMM yyyy'); } function formatLongDate(value: string) { return format(new Date(value), 'MMM dd, yyyy'); } function formatDateTime(value: string) { return format(new Date(value), 'MMM dd, yyyy · HH:mm'); } function ratioPercent(numerator: number | null, denominator: number | null) { if (numerator === null || denominator === null || denominator === 0) { return null; } return (numerator / denominator) * 100; } function normalizeTickerInput(value: string | null) { const normalized = value?.trim().toUpperCase() ?? ''; return normalized || null; } function isFinancialSnapshotForm( filingType: CompanyAnalysis['filings'][number]['filing_type'] ): filingType is '10-K' | '10-Q' { return filingType === '10-K' || filingType === '10-Q'; } function includesFinancialPeriod(filingType: '10-K' | '10-Q', filter: FinancialPeriodFilter) { if (filter === 'quarterlyOnly') { return filingType === '10-Q'; } if (filter === 'fiscalYearEndOnly') { return filingType === '10-K'; } return true; } function asScaledFinancialCurrency( value: number | null | undefined, scale: NumberScaleUnit ) { if (value === null || value === undefined) { return 'n/a'; } return formatCurrencyByScale(value, scale); } export default function AnalysisPage() { return ( Loading analysis desk...}> ); } function AnalysisPageContent() { const { isPending, isAuthenticated } = useAuthGuard(); const searchParams = useSearchParams(); const queryClient = useQueryClient(); const { prefetchReport, prefetchResearchTicker } = useLinkPrefetch(); const initialTicker = normalizeTickerInput(searchParams.get('ticker')) ?? 'MSFT'; const [tickerInput, setTickerInput] = useState(initialTicker); const [ticker, setTicker] = useState(initialTicker); const [analysis, setAnalysis] = useState(null); const [journalEntries, setJournalEntries] = useState([]); const [journalLoading, setJournalLoading] = useState(true); const [journalForm, setJournalForm] = useState(EMPTY_JOURNAL_FORM); const [editingJournalId, setEditingJournalId] = useState(null); const [highlightedJournalId, setHighlightedJournalId] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [financialPeriodFilter, setFinancialPeriodFilter] = useState('quarterlyAndFiscalYearEnd'); const [financialValueScale, setFinancialValueScale] = useState('millions'); const journalEntryRefs = useRef(new Map()); useEffect(() => { const normalized = normalizeTickerInput(searchParams.get('ticker')); if (!normalized) { return; } setTickerInput(normalized); setTicker(normalized); const journalId = Number(searchParams.get('journalId')); setHighlightedJournalId(Number.isInteger(journalId) && journalId > 0 ? journalId : null); }, [searchParams]); const loadAnalysis = useCallback(async (symbol: string) => { const options = companyAnalysisQueryOptions(symbol); if (!queryClient.getQueryData(options.queryKey)) { setLoading(true); } setError(null); try { const response = await queryClient.fetchQuery(options); setAnalysis(response.analysis); } catch (err) { setError(err instanceof Error ? err.message : 'Unable to load company analysis'); setAnalysis(null); } finally { setLoading(false); } }, [queryClient]); const loadJournal = useCallback(async (symbol: string) => { const options = researchJournalQueryOptions(symbol); if (!queryClient.getQueryData(options.queryKey)) { setJournalLoading(true); } try { const response = await queryClient.fetchQuery(options); setJournalEntries(response.entries); } catch (err) { setError(err instanceof Error ? err.message : 'Unable to load research journal'); setJournalEntries([]); } finally { setJournalLoading(false); } }, [queryClient]); useEffect(() => { if (!isPending && isAuthenticated) { void Promise.all([ loadAnalysis(ticker), loadJournal(ticker) ]); } }, [isPending, isAuthenticated, loadAnalysis, loadJournal, ticker]); useEffect(() => { if (!highlightedJournalId) { return; } const node = journalEntryRefs.current.get(highlightedJournalId); if (!node) { return; } node.scrollIntoView({ behavior: 'smooth', block: 'center' }); const timeoutId = window.setTimeout(() => { setHighlightedJournalId((current) => (current === highlightedJournalId ? null : current)); }, 2200); return () => { window.clearTimeout(timeoutId); }; }, [highlightedJournalId, journalEntries]); const priceSeries = useMemo(() => { return (analysis?.priceHistory ?? []).map((point) => ({ ...point, label: formatShortDate(point.date) })); }, [analysis?.priceHistory]); const financialSeries = useMemo(() => { return (analysis?.financials ?? []) .filter((item): item is CompanyAnalysis['financials'][number] & { filingType: '10-K' | '10-Q' } => { return isFinancialSnapshotForm(item.filingType); }) .slice() .sort((a, b) => Date.parse(a.filingDate) - Date.parse(b.filingDate)) .map((item) => ({ filingDate: item.filingDate, filingType: item.filingType, periodLabel: item.filingType === '10-Q' ? 'Quarter End' : 'Fiscal Year End', revenue: item.revenue, netIncome: item.netIncome, assets: item.totalAssets, netMargin: ratioPercent(item.netIncome ?? null, item.revenue ?? null) })); }, [analysis?.financials]); const filteredFinancialSeries = useMemo(() => { return financialSeries.filter((point) => includesFinancialPeriod(point.filingType, financialPeriodFilter)); }, [financialSeries, financialPeriodFilter]); const periodEndFilings = useMemo(() => { return (analysis?.filings ?? []).filter((filing) => isFinancialSnapshotForm(filing.filing_type)); }, [analysis?.filings]); const selectedFinancialScaleLabel = useMemo(() => { return FINANCIAL_VALUE_SCALE_OPTIONS.find((option) => option.value === financialValueScale)?.label ?? 'Millions (M)'; }, [financialValueScale]); const resetJournalForm = useCallback(() => { setEditingJournalId(null); setJournalForm(EMPTY_JOURNAL_FORM); }, []); const activeTicker = analysis?.company.ticker ?? ticker; const saveJournalEntry = async (event: React.FormEvent) => { event.preventDefault(); const normalizedTicker = activeTicker.trim().toUpperCase(); if (!normalizedTicker) { return; } setError(null); try { if (editingJournalId === null) { await createResearchJournalEntry({ ticker: normalizedTicker, entryType: journalForm.accessionNumber.trim() ? 'filing_note' : 'note', title: journalForm.title.trim() || undefined, bodyMarkdown: journalForm.bodyMarkdown, accessionNumber: journalForm.accessionNumber.trim() || undefined }); } else { await updateResearchJournalEntry(editingJournalId, { title: journalForm.title.trim() || undefined, bodyMarkdown: journalForm.bodyMarkdown }); } void queryClient.invalidateQueries({ queryKey: queryKeys.researchJournal(normalizedTicker) }); void queryClient.invalidateQueries({ queryKey: queryKeys.companyAnalysis(normalizedTicker) }); void queryClient.invalidateQueries({ queryKey: queryKeys.watchlist() }); await Promise.all([ loadAnalysis(normalizedTicker), loadJournal(normalizedTicker) ]); resetJournalForm(); } catch (err) { setError(err instanceof Error ? err.message : 'Unable to save journal entry'); } }; const beginEditJournalEntry = (entry: ResearchJournalEntry) => { setEditingJournalId(entry.id); setJournalForm({ title: entry.title ?? '', bodyMarkdown: entry.body_markdown, accessionNumber: entry.accession_number ?? '' }); }; const removeJournalEntry = async (entry: ResearchJournalEntry) => { try { await deleteResearchJournalEntry(entry.id); void queryClient.invalidateQueries({ queryKey: queryKeys.researchJournal(entry.ticker) }); void queryClient.invalidateQueries({ queryKey: queryKeys.companyAnalysis(entry.ticker) }); await Promise.all([ loadAnalysis(entry.ticker), loadJournal(entry.ticker) ]); if (editingJournalId === entry.id) { resetJournalForm(); } } catch (err) { setError(err instanceof Error ? err.message : 'Unable to delete journal entry'); } }; if (isPending || !isAuthenticated) { return
Loading analysis desk...
; } return ( Ask with RAG )} >
{ event.preventDefault(); const normalized = tickerInput.trim().toUpperCase(); if (!normalized) { return; } setTicker(normalized); }} > setTickerInput(event.target.value.toUpperCase())} placeholder="Ticker (AAPL)" className="w-full sm:max-w-xs" /> {analysis ? ( <> prefetchResearchTicker(analysis.company.ticker)} onFocus={() => prefetchResearchTicker(analysis.company.ticker)} className="text-sm text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]" > Open financials prefetchResearchTicker(analysis.company.ticker)} onFocus={() => prefetchResearchTicker(analysis.company.ticker)} className="text-sm text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]" > Open filing stream prefetchResearchTicker(analysis.company.ticker)} onFocus={() => prefetchResearchTicker(analysis.company.ticker)} className="text-sm text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]" > Open graphing ) : null}
{error ? (

{error}

) : null}

{analysis?.company.companyName ?? ticker}

{analysis?.company.ticker ?? ticker}

{analysis?.company.sector ?? 'Sector unavailable'}

{analysis?.company.category ? (

{analysis.company.category}

) : null} {analysis?.company.tags.length ? (
{analysis.company.tags.map((tag) => ( {tag} ))}
) : null}

{formatCurrency(analysis?.quote ?? 0)}

CIK {analysis?.company.cik ?? 'n/a'}

{formatCurrency(analysis?.position?.market_value)}

{analysis?.position ? `${asNumber(analysis.position.shares).toLocaleString()} shares` : 'Not held in portfolio'}

= 0 ? 'text-[#96f5bf]' : 'text-[#ff9f9f]'}`}> {formatCurrency(analysis?.position?.gain_loss)}

{formatPercent(analysis?.position?.gain_loss_pct)}

{analysis?.coverage ? (

Status

{analysis.coverage.status}

Priority

{analysis.coverage.priority}

Last Reviewed

{analysis.coverage.last_reviewed_at ? formatDateTime(analysis.coverage.last_reviewed_at) : 'No research review recorded yet'}

Latest Filing

{analysis.coverage.latest_filing_date ? formatLongDate(analysis.coverage.latest_filing_date) : 'No filing history loaded yet'}

Manage coverage
) : (

This company is not yet in your coverage list. Add it from Coverage to track workflow status and review cadence.

)}

Revenue

{asScaledFinancialCurrency(analysis?.keyMetrics.revenue, financialValueScale)}

Net Income

{asScaledFinancialCurrency(analysis?.keyMetrics.netIncome, financialValueScale)}

Assets

{asScaledFinancialCurrency(analysis?.keyMetrics.totalAssets, financialValueScale)}

Cash / Debt

{analysis?.keyMetrics.cash != null && analysis?.keyMetrics.debt != null ? `${asScaledFinancialCurrency(analysis.keyMetrics.cash, financialValueScale)} / ${asScaledFinancialCurrency(analysis.keyMetrics.debt, financialValueScale)}` : 'n/a'}

Reference date: {analysis?.keyMetrics.referenceDate ? formatLongDate(analysis.keyMetrics.referenceDate) : 'No financial filing selected'}. {' '}Net margin: {analysis && analysis.keyMetrics.netMargin !== null ? formatPercent(analysis.keyMetrics.netMargin) : 'n/a'}.

{analysis?.latestFilingSummary ? (

Filing

{analysis.latestFilingSummary.filingType} · {formatLongDate(analysis.latestFilingSummary.filingDate)}

{analysis.latestFilingSummary.summary ?? 'No AI summary stored yet for the most recent filing.'}

{analysis.latestFilingSummary.hasAnalysis ? ( Open latest memo ) : null} Open filing stream
) : (

No filing snapshot available yet for this company.

)}
{loading ? (

Loading price history...

) : priceSeries.length === 0 ? (

No price history available.

) : (
`$${value.toFixed(0)}`} /> formatCurrency(Array.isArray(value) ? value[0] : value)} contentStyle={{ backgroundColor: CHART_TOOLTIP_BG, border: `1px solid ${CHART_TOOLTIP_BORDER}`, borderRadius: '0.75rem' }} labelStyle={{ color: CHART_TEXT }} itemStyle={{ color: CHART_TEXT }} cursor={{ stroke: 'rgba(220, 226, 234, 0.28)', strokeWidth: 1 }} />
)}
{FINANCIAL_PERIOD_FILTER_OPTIONS.map((option) => ( ))}
{FINANCIAL_VALUE_SCALE_OPTIONS.map((option) => ( ))}
)} > {loading ? (

Loading financials...

) : filteredFinancialSeries.length === 0 ? (

No financial rows match the selected period filter.

) : (
{filteredFinancialSeries.map((point, index) => (

{formatLongDate(point.filingDate)}

{point.filingType} · {point.periodLabel}

= 0 ? 'text-[#96f5bf]' : 'text-[#ff9f9f]'}`}> {asScaledFinancialCurrency(point.netIncome, financialValueScale)}

Revenue
{asScaledFinancialCurrency(point.revenue, financialValueScale)}
Assets
{asScaledFinancialCurrency(point.assets, financialValueScale)}
Net Margin
{point.netMargin === null ? 'n/a' : formatPercent(point.netMargin)}
))}
{filteredFinancialSeries.map((point, index) => ( ))}
Filed Period Form Revenue Net Income Assets Net Margin
{formatLongDate(point.filingDate)} {point.periodLabel} {point.filingType} {asScaledFinancialCurrency(point.revenue, financialValueScale)} = 0 ? 'text-[#96f5bf]' : 'text-[#ff9f9f]'}> {asScaledFinancialCurrency(point.netIncome, financialValueScale)} {asScaledFinancialCurrency(point.assets, financialValueScale)} {point.netMargin === null ? 'n/a' : formatPercent(point.netMargin)}
)} {loading ? (

Loading filings...

) : periodEndFilings.length === 0 ? (

No 10-K or 10-Q filings available for this ticker.

) : (
{periodEndFilings.map((filing) => (

{format(new Date(filing.filing_date), 'MMM dd, yyyy')}

{filing.filing_type} · {filing.filing_type === '10-Q' ? 'Quarter End' : 'Fiscal Year End'}

{filing.filing_url ? ( SEC filing ) : null}
Revenue
{asScaledFinancialCurrency(filing.metrics?.revenue, financialValueScale)}
Net Income
{asScaledFinancialCurrency(filing.metrics?.netIncome, financialValueScale)}
Assets
{asScaledFinancialCurrency(filing.metrics?.totalAssets, financialValueScale)}
))}
{periodEndFilings.map((filing) => ( ))}
Filed Period Type Revenue Net Income Assets Document
{format(new Date(filing.filing_date), 'MMM dd, yyyy')} {filing.filing_type === '10-Q' ? 'Quarter End' : 'Fiscal Year End'} {filing.filing_type} {asScaledFinancialCurrency(filing.metrics?.revenue, financialValueScale)} {asScaledFinancialCurrency(filing.metrics?.netIncome, financialValueScale)} {asScaledFinancialCurrency(filing.metrics?.totalAssets, financialValueScale)} {filing.filing_url ? ( SEC filing ) : ( 'n/a' )}
)}
{loading ? (

Loading AI reports...

) : !analysis || analysis.recentAiReports.length === 0 ? (

No AI reports generated yet. Run filing analysis from the filings stream.

) : (
{analysis.recentAiReports.map((report) => (

{report.filingType} · {format(new Date(report.filingDate), 'MMM dd, yyyy')}

{report.provider} / {report.model}

{report.summary}

{report.accessionNumber}

prefetchReport(analysis.company.ticker, report.accessionNumber)} onFocus={() => prefetchReport(analysis.company.ticker, report.accessionNumber)} className="text-xs uppercase tracking-[0.12em] text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]" > Open summary
))}
)}
prefetchResearchTicker(activeTicker)} onFocus={() => prefetchResearchTicker(activeTicker)} className="inline-flex items-center gap-2 rounded-lg border border-[color:var(--line-weak)] px-3 py-2 text-xs uppercase tracking-[0.12em] text-[color:var(--accent)] transition hover:border-[color:var(--line-strong)] hover:text-[color:var(--accent-strong)]" > Open research )} >

Workspace focus

Use the research surface to manage the typed library, attach evidence to memo sections, upload diligence files, and assemble the packet view for investor review.

Stored research entries

{journalEntries.length}

Latest update

{journalEntries[0] ? formatDateTime(journalEntries[0].updated_at) : 'No research activity yet'}

{journalLoading ? (

Loading research entries...

) : journalEntries.length === 0 ? (

No research saved yet. Use AI memo saves from reports or open the Research workspace to start building the thesis.

) : (
{journalEntries.slice(0, 4).map((entry) => (

{entry.entry_type.replace('_', ' ')} · {formatDateTime(entry.updated_at)}

{entry.title ?? 'Untitled entry'}

prefetchResearchTicker(activeTicker)} onFocus={() => prefetchResearchTicker(activeTicker)} className="text-xs uppercase tracking-[0.12em] text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]" > Open workspace

{entry.body_markdown}

))}
)}
Analysis scope: price + filings + ai synthesis + research workspace
); }