From 0615534f4b03ee5980419ce2990006bd28391bfb Mon Sep 17 00:00:00 2001 From: francy51 Date: Sat, 28 Feb 2026 15:13:21 -0500 Subject: [PATCH] chore: commit all current changes --- app/analysis/page.tsx | 9 + .../[ticker]/[accessionNumber]/page.tsx | 154 ++++++++++++++++++ app/filings/page.tsx | 35 +++- lib/api.ts | 8 + lib/server/api/app.ts | 43 ++++- lib/types.ts | 8 + 6 files changed, 248 insertions(+), 9 deletions(-) create mode 100644 app/analysis/reports/[ticker]/[accessionNumber]/page.tsx diff --git a/app/analysis/page.tsx b/app/analysis/page.tsx index ab234ec..a4c0835 100644 --- a/app/analysis/page.tsx +++ b/app/analysis/page.tsx @@ -286,6 +286,15 @@ function AnalysisPageContent() {

{report.summary}

+
+

{report.accessionNumber}

+ + Open summary + +
))} diff --git a/app/analysis/reports/[ticker]/[accessionNumber]/page.tsx b/app/analysis/reports/[ticker]/[accessionNumber]/page.tsx new file mode 100644 index 0000000..7b8b0b8 --- /dev/null +++ b/app/analysis/reports/[ticker]/[accessionNumber]/page.tsx @@ -0,0 +1,154 @@ +'use client'; + +import Link from 'next/link'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { format } from 'date-fns'; +import { ArrowLeft, BrainCircuit, RefreshCcw } from 'lucide-react'; +import { useParams } from 'next/navigation'; +import { AppShell } from '@/components/shell/app-shell'; +import { useAuthGuard } from '@/hooks/use-auth-guard'; +import { getCompanyAiReport } from '@/lib/api'; +import type { CompanyAiReportDetail } from '@/lib/types'; +import { Button } from '@/components/ui/button'; +import { Panel } from '@/components/ui/panel'; + +function formatFilingDate(value: string) { + const date = new Date(value); + + if (Number.isNaN(date.getTime())) { + return 'Unknown'; + } + + return format(date, 'MMM dd, yyyy'); +} + +export default function AnalysisReportPage() { + const { isPending, isAuthenticated } = useAuthGuard(); + const params = useParams<{ ticker: string; accessionNumber: string }>(); + + const tickerFromRoute = useMemo(() => { + const value = typeof params.ticker === 'string' ? params.ticker : ''; + return value.toUpperCase(); + }, [params.ticker]); + + const accessionNumber = useMemo(() => { + const value = typeof params.accessionNumber === 'string' ? params.accessionNumber : ''; + return decodeURIComponent(value); + }, [params.accessionNumber]); + + const [report, setReport] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const loadReport = useCallback(async () => { + if (!accessionNumber) { + setError('Invalid accession number.'); + setReport(null); + setLoading(false); + return; + } + + setLoading(true); + setError(null); + + try { + const response = await getCompanyAiReport(accessionNumber); + setReport(response.report); + } catch (err) { + setReport(null); + setError(err instanceof Error ? err.message : 'Unable to load AI summary'); + } finally { + setLoading(false); + } + }, [accessionNumber]); + + useEffect(() => { + if (!isPending && isAuthenticated) { + void loadReport(); + } + }, [isPending, isAuthenticated, loadReport]); + + if (isPending || !isAuthenticated) { + return
Loading summary detail...
; + } + + const resolvedTicker = report?.ticker ?? tickerFromRoute; + + return ( + void loadReport()} disabled={loading}> + + Refresh + + )} + > + +
+ + + Back to analysis + + + + Back to filings + +
+
+ + {loading ? ( + +

Loading AI summary...

+
+ ) : null} + + {!loading && error ? ( + +

{error}

+
+ ) : null} + + {!loading && !error && report ? ( + <> + +
+
+

Accession

+

{report.accessionNumber}

+
+
+

Provider

+

{report.provider}

+
+
+

Model

+

{report.model}

+
+
+
+ + +
+ + Full text view +
+

+ {report.summary} +

+
+ + ) : null} +
+ ); +} diff --git a/app/filings/page.tsx b/app/filings/page.tsx index ea20b27..6d90c4c 100644 --- a/app/filings/page.tsx +++ b/app/filings/page.tsx @@ -1,5 +1,6 @@ 'use client'; +import Link from 'next/link'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { Suspense } from 'react'; import { format } from 'date-fns'; @@ -300,6 +301,14 @@ function FilingsPageContent() { Analyze + {hasAnalysis ? ( + + Open summary + + ) : null} ); @@ -350,14 +359,24 @@ function FilingsPageContent() { - +
+ + {hasAnalysis ? ( + + Summary + + ) : null} +
); diff --git a/lib/api.ts b/lib/api.ts index 47504d9..d0a69fe 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -1,6 +1,7 @@ import { edenTreaty } from '@elysiajs/eden'; import type { App } from '@/lib/server/api/app'; import type { + CompanyAiReportDetail, CompanyAnalysis, Filing, Holding, @@ -184,6 +185,13 @@ export async function getCompanyAnalysis(ticker: string) { return await unwrapData<{ analysis: CompanyAnalysis }>(result, 'Unable to fetch company analysis'); } +export async function getCompanyAiReport(accessionNumber: string) { + const normalizedAccession = accessionNumber.trim(); + + const result = await client.api.analysis.reports[normalizedAccession].get(); + return await unwrapData<{ report: CompanyAiReportDetail }>(result, 'Unable to fetch AI summary'); +} + export async function queueFilingSync(input: { ticker: string; limit?: number }) { const result = await client.api.filings.sync.post(input); return await unwrapData<{ task: Task }>(result, 'Unable to queue filing sync'); diff --git a/lib/server/api/app.ts b/lib/server/api/app.ts index ceef3cd..c82b77d 100644 --- a/lib/server/api/app.ts +++ b/lib/server/api/app.ts @@ -4,7 +4,7 @@ import { auth } from '@/lib/auth'; import { requireAuthenticatedSession } from '@/lib/server/auth-session'; import { asErrorMessage, jsonError } from '@/lib/server/http'; import { buildPortfolioSummary } from '@/lib/server/portfolio'; -import { listFilingsRecords } from '@/lib/server/repos/filings'; +import { getFilingByAccession, listFilingsRecords } from '@/lib/server/repos/filings'; import { deleteHoldingByIdRecord, listUserHoldings, @@ -386,6 +386,47 @@ export const app = new Elysia({ prefix: '/api' }) ticker: t.String({ minLength: 1 }) }) }) + .get('/analysis/reports/:accessionNumber', async ({ params }) => { + const { response } = await requireAuthenticatedSession(); + if (response) { + return response; + } + + const accessionNumber = params.accessionNumber?.trim() ?? ''; + if (accessionNumber.length < 4) { + return jsonError('Invalid accession number'); + } + + const filing = await getFilingByAccession(accessionNumber); + if (!filing) { + return jsonError('AI summary not found', 404); + } + + const summary = filing.analysis?.text ?? filing.analysis?.legacyInsights ?? ''; + if (!summary) { + return jsonError('AI summary not found', 404); + } + + return Response.json({ + report: { + accessionNumber: filing.accession_number, + ticker: filing.ticker, + companyName: filing.company_name, + filingDate: filing.filing_date, + filingType: filing.filing_type, + provider: filing.analysis?.provider ?? 'unknown', + model: filing.analysis?.model ?? 'unknown', + summary, + filingUrl: filing.filing_url, + submissionUrl: filing.submission_url ?? null, + primaryDocument: filing.primary_document ?? null + } + }); + }, { + params: t.Object({ + accessionNumber: t.String({ minLength: 4 }) + }) + }) .get('/filings', async ({ query }) => { const { response } = await requireAuthenticatedSession(); if (response) { diff --git a/lib/types.ts b/lib/types.ts index f562507..e827ea3 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -113,6 +113,14 @@ export type CompanyAiReport = { summary: string; }; +export type CompanyAiReportDetail = CompanyAiReport & { + ticker: string; + companyName: string; + filingUrl: string | null; + submissionUrl: string | null; + primaryDocument: string | null; +}; + export type CompanyAnalysis = { company: { ticker: string;