Add research workspace and graphing flows

This commit is contained in:
2026-03-07 16:52:35 -05:00
parent db01f207a5
commit 62bacdf104
37 changed files with 5494 additions and 434 deletions

View File

@@ -2,7 +2,7 @@
import { useQueryClient } from '@tanstack/react-query';
import type { LucideIcon } from 'lucide-react';
import { Activity, BookOpenText, ChartCandlestick, Eye, Landmark, LineChart, LogOut, Menu } from 'lucide-react';
import { Activity, BarChart3, BookOpenText, ChartCandlestick, Eye, Landmark, LineChart, LogOut, Menu, NotebookTabs } from 'lucide-react';
import Link from 'next/link';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
@@ -11,6 +11,7 @@ import { TaskDetailModal } from '@/components/notifications/task-detail-modal';
import { TaskNotificationsTrigger } from '@/components/notifications/task-notifications-trigger';
import {
companyAnalysisQueryOptions,
companyFinancialStatementsQueryOptions,
filingsQueryOptions,
holdingsQueryOptions,
latestPortfolioInsightQueryOptions,
@@ -18,6 +19,7 @@ import {
recentTasksQueryOptions,
watchlistQueryOptions
} from '@/lib/query/options';
import { buildGraphingHref } from '@/lib/graphing/catalog';
import type { ActiveContext, NavGroup, NavItem } from '@/lib/types';
import { Button } from '@/components/ui/button';
import { useTaskNotificationsCenter } from '@/hooks/use-task-notifications-center';
@@ -56,6 +58,26 @@ const NAV_ITEMS: NavConfigItem[] = [
preserveTicker: true,
mobilePrimary: true
},
{
id: 'research',
href: '/research',
label: 'Research',
icon: NotebookTabs,
group: 'research',
matchMode: 'exact',
preserveTicker: true,
mobilePrimary: true
},
{
id: 'graphing',
href: '/graphing',
label: 'Graphing',
icon: BarChart3,
group: 'research',
matchMode: 'exact',
preserveTicker: true,
mobilePrimary: false
},
{
id: 'financials',
href: '/financials',
@@ -117,6 +139,10 @@ function toTickerHref(baseHref: string, activeTicker: string | null) {
}
function resolveNavHref(item: NavItem, context: ActiveContext) {
if (item.href === '/graphing') {
return buildGraphingHref(context.activeTicker);
}
if (!item.preserveTicker) {
return item.href;
}
@@ -134,6 +160,8 @@ function isItemActive(item: NavItem, pathname: string) {
function buildDefaultBreadcrumbs(pathname: string, activeTicker: string | null) {
const analysisHref = toTickerHref('/analysis', activeTicker);
const researchHref = toTickerHref('/research', activeTicker);
const graphingHref = buildGraphingHref(activeTicker);
const financialsHref = toTickerHref('/financials', activeTicker);
const filingsHref = toTickerHref('/filings', activeTicker);
@@ -153,6 +181,13 @@ function buildDefaultBreadcrumbs(pathname: string, activeTicker: string | null)
return [{ label: 'Analysis' }];
}
if (pathname.startsWith('/research')) {
return [
{ label: 'Analysis', href: analysisHref },
{ label: 'Research', href: researchHref }
];
}
if (pathname.startsWith('/financials')) {
return [
{ label: 'Analysis', href: analysisHref },
@@ -160,6 +195,14 @@ function buildDefaultBreadcrumbs(pathname: string, activeTicker: string | null)
];
}
if (pathname.startsWith('/graphing')) {
return [
{ label: 'Analysis', href: analysisHref },
{ label: 'Graphing', href: graphingHref },
{ label: activeTicker ?? 'Compare Set' }
];
}
if (pathname.startsWith('/filings')) {
return [
{ label: 'Analysis', href: analysisHref },
@@ -281,6 +324,20 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
return;
}
if (href.startsWith('/graphing')) {
if (context.activeTicker) {
void queryClient.prefetchQuery(companyFinancialStatementsQueryOptions({
ticker: context.activeTicker,
surfaceKind: 'income_statement',
cadence: 'annual',
includeDimensions: false,
includeFacts: false,
limit: 16
}));
}
return;
}
if (href.startsWith('/filings')) {
void queryClient.prefetchQuery(filingsQueryOptions({
ticker: context.activeTicker ?? undefined,
@@ -317,7 +374,7 @@ export function AppShell({ title, subtitle, actions, activeTicker, breadcrumbs,
};
const runPrefetch = () => {
const prioritized = navEntries.filter((entry) => entry.id === 'analysis' || entry.id === 'filings' || entry.id === 'portfolio');
const prioritized = navEntries.filter((entry) => entry.id === 'analysis' || entry.id === 'graphing' || entry.id === 'filings' || entry.id === 'portfolio');
for (const entry of prioritized) {
prefetchForHref(entry.href);
}