Add history window controls and expand taxonomy pack support
- add 3Y/5Y/10Y financial history filtering and reorganize normalization details UI - add new fiscal taxonomy surface/income bridge/KPI packs and update Rust taxonomy loading - auto-detect Homebrew SQLite for native `sqlite-vec` in local dev/e2e with docs and env guidance
This commit is contained in:
@@ -14,7 +14,6 @@ import {
|
||||
import { format } from "date-fns";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import {
|
||||
Bar,
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
@@ -25,6 +24,7 @@ import {
|
||||
} from "recharts";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Download,
|
||||
RefreshCcw,
|
||||
@@ -57,6 +57,11 @@ import {
|
||||
resolveStatementSelection,
|
||||
type StatementInspectorSelection,
|
||||
} from "@/lib/financials/statement-view-model";
|
||||
import {
|
||||
filterPeriodsByHistoryWindow,
|
||||
financialHistoryLimit,
|
||||
type FinancialHistoryWindow,
|
||||
} from "@/lib/financials/history-window";
|
||||
import { queryKeys } from "@/lib/query/keys";
|
||||
import { companyFinancialStatementsQueryOptions } from "@/lib/query/options";
|
||||
import type {
|
||||
@@ -65,7 +70,6 @@ import type {
|
||||
DerivedFinancialRow,
|
||||
FinancialCadence,
|
||||
FinancialDisplayMode,
|
||||
FinancialStatementPeriod,
|
||||
FinancialSurfaceKind,
|
||||
FinancialUnit,
|
||||
RatioRow,
|
||||
@@ -73,7 +77,6 @@ import type {
|
||||
StructuredKpiRow,
|
||||
SurfaceFinancialRow,
|
||||
TaxonomyStatementRow,
|
||||
TrendSeries,
|
||||
} from "@/lib/types";
|
||||
|
||||
type LoadOptions = {
|
||||
@@ -111,6 +114,15 @@ const CADENCE_OPTIONS = [
|
||||
{ value: "ltm", label: "LTM" },
|
||||
];
|
||||
|
||||
const HISTORY_WINDOW_OPTIONS: Array<{
|
||||
value: `${FinancialHistoryWindow}`;
|
||||
label: string;
|
||||
}> = [
|
||||
{ value: "3", label: "3Y" },
|
||||
{ value: "5", label: "5Y" },
|
||||
{ value: "10", label: "10Y" },
|
||||
];
|
||||
|
||||
const DISPLAY_MODE_OPTIONS = [
|
||||
{ value: "standardized", label: "Standard" },
|
||||
{ value: "faithful", label: "Faithful" },
|
||||
@@ -410,6 +422,59 @@ function ChartFrame({ children }: { children: React.ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsibleSection(props: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
children: React.ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(props.defaultOpen ?? false);
|
||||
|
||||
return (
|
||||
<section className="border-t border-[color:var(--line-weak)] pt-4">
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-start justify-between gap-3 rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel)] px-3 py-2 text-left transition hover:border-[color:var(--line-strong)]"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-semibold text-[color:var(--terminal-bright)]">
|
||||
{props.title}
|
||||
</span>
|
||||
{props.subtitle ? (
|
||||
<span className="mt-0.5 block text-xs text-[color:var(--terminal-muted)]">
|
||||
{props.subtitle}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="mt-0.5 shrink-0 text-[color:var(--terminal-muted)]">
|
||||
{open ? (
|
||||
<ChevronDown className="size-4" />
|
||||
) : (
|
||||
<ChevronRight className="size-4" />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
{open ? <div className="pt-4">{props.children}</div> : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function SummarySubsection(props: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-[color:var(--terminal-bright)]">
|
||||
{props.title}
|
||||
</h3>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FinancialsPageContent() {
|
||||
const { isPending, isAuthenticated } = useAuthGuard();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -423,6 +488,7 @@ function FinancialsPageContent() {
|
||||
const [cadence, setCadence] = useState<FinancialCadence>("annual");
|
||||
const [displayMode, setDisplayMode] =
|
||||
useState<FinancialDisplayMode>("standardized");
|
||||
const [historyWindow, setHistoryWindow] = useState<FinancialHistoryWindow>(3);
|
||||
const [valueScale, setValueScale] = useState<NumberScaleUnit>("millions");
|
||||
const [rowSearch, setRowSearch] = useState("");
|
||||
const [showPercentChange, setShowPercentChange] = useState(false);
|
||||
@@ -438,10 +504,14 @@ function FinancialsPageContent() {
|
||||
() => new Set(),
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [, setLoadingMore] = useState(false);
|
||||
const [syncingFinancials, setSyncingFinancials] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showTrendChart, setShowTrendChart] = useState(false);
|
||||
const historyFetchLimit = useMemo(
|
||||
() => financialHistoryLimit(cadence, historyWindow),
|
||||
[cadence, historyWindow],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fromQuery = searchParams.get("ticker");
|
||||
@@ -509,7 +579,7 @@ function FinancialsPageContent() {
|
||||
includeDimensions,
|
||||
includeFacts: false,
|
||||
cursor: nextCursor,
|
||||
limit: 12,
|
||||
limit: historyFetchLimit,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -532,7 +602,14 @@ function FinancialsPageContent() {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
},
|
||||
[cadence, queryClient, selectedFlatRowKey, selectedRowRef, surfaceKind],
|
||||
[
|
||||
cadence,
|
||||
historyFetchLimit,
|
||||
queryClient,
|
||||
selectedFlatRowKey,
|
||||
selectedRowRef,
|
||||
surfaceKind,
|
||||
],
|
||||
);
|
||||
|
||||
const syncFinancials = useCallback(async () => {
|
||||
@@ -578,6 +655,10 @@ function FinancialsPageContent() {
|
||||
Date.parse(right.periodEnd ?? right.filingDate),
|
||||
);
|
||||
}, [financials?.periods]);
|
||||
const visiblePeriods = useMemo(
|
||||
() => filterPeriodsByHistoryWindow(periods, cadence, historyWindow),
|
||||
[cadence, historyWindow, periods],
|
||||
);
|
||||
|
||||
const isTreeStatementMode =
|
||||
displayMode === "standardized" && isStatementSurfaceKind(surfaceKind);
|
||||
@@ -708,11 +789,23 @@ function FinancialsPageContent() {
|
||||
surfaceKind === "income_statement" ||
|
||||
surfaceKind === "cash_flow_statement"
|
||||
) {
|
||||
return standardizedRows.find((row) => row.key === "revenue") ?? null;
|
||||
return (
|
||||
standardizedRows.find((row) => row.key === "revenue") ??
|
||||
standardizedRows.find((row) => row.key === "net_cash_from_operating") ??
|
||||
standardizedRows.find((row) => row.key === "operating_income") ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
if (surfaceKind === "balance_sheet") {
|
||||
return standardizedRows.find((row) => row.key === "total_assets") ?? null;
|
||||
return (
|
||||
standardizedRows.find((row) => row.key === "total_assets") ??
|
||||
standardizedRows.find(
|
||||
(row) => row.key === "total_liabilities_and_equity",
|
||||
) ??
|
||||
standardizedRows.find((row) => row.key === "stockholders_equity") ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -724,7 +817,7 @@ function FinancialsPageContent() {
|
||||
|
||||
const trendSeries = financials?.trendSeries ?? [];
|
||||
const chartData = useMemo(() => {
|
||||
return periods.map((period) => ({
|
||||
return visiblePeriods.map((period) => ({
|
||||
label: formatLongDate(period.periodEnd ?? period.filingDate),
|
||||
...Object.fromEntries(
|
||||
trendSeries.map((series) => [
|
||||
@@ -733,7 +826,7 @@ function FinancialsPageContent() {
|
||||
]),
|
||||
),
|
||||
}));
|
||||
}, [periods, trendSeries]);
|
||||
}, [trendSeries, visiblePeriods]);
|
||||
|
||||
const controlSections = useMemo<FinancialControlSection[]>(() => {
|
||||
const sections: FinancialControlSection[] = [
|
||||
@@ -761,6 +854,16 @@ function FinancialsPageContent() {
|
||||
setExpandedRowKeys(new Set());
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "history",
|
||||
label: "History",
|
||||
options: HISTORY_WINDOW_OPTIONS,
|
||||
value: String(historyWindow),
|
||||
onChange: (value) =>
|
||||
setHistoryWindow(
|
||||
Number.parseInt(value, 10) as FinancialHistoryWindow,
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (
|
||||
@@ -791,7 +894,7 @@ function FinancialsPageContent() {
|
||||
});
|
||||
|
||||
return sections;
|
||||
}, [cadence, displayMode, surfaceKind, valueScale]);
|
||||
}, [cadence, displayMode, historyWindow, surfaceKind, valueScale]);
|
||||
|
||||
const toggleExpandedRow = useCallback((key: string) => {
|
||||
setExpandedRowKeys((current) => {
|
||||
@@ -1069,7 +1172,7 @@ function FinancialsPageContent() {
|
||||
edits or derived rows are available yet.
|
||||
</p>
|
||||
</div>
|
||||
) : periods.length === 0 ||
|
||||
) : visiblePeriods.length === 0 ||
|
||||
(isTreeStatementMode
|
||||
? (treeModel?.visibleNodeCount ?? 0) === 0
|
||||
: filteredRows.length === 0) ? (
|
||||
@@ -1081,7 +1184,7 @@ function FinancialsPageContent() {
|
||||
{isStatementSurfaceKind(surfaceKind) ? (
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs uppercase tracking-[0.24em] text-[color:var(--terminal-muted)]">
|
||||
USD · {valueScaleLabel}
|
||||
USD · {valueScaleLabel} · Latest {historyWindow} years
|
||||
</p>
|
||||
{isTreeStatementMode && hasUnmappedResidualRows ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">
|
||||
@@ -1096,7 +1199,7 @@ function FinancialsPageContent() {
|
||||
) : null}
|
||||
{isTreeStatementMode && treeModel ? (
|
||||
<StatementMatrix
|
||||
periods={periods}
|
||||
periods={visiblePeriods}
|
||||
sections={treeModel.sections}
|
||||
selectedRowRef={selectedRowRef}
|
||||
onToggleRow={toggleExpandedRow}
|
||||
@@ -1104,17 +1207,14 @@ function FinancialsPageContent() {
|
||||
renderCellValue={renderStatementTreeCellValue}
|
||||
periodLabelFormatter={formatLongDate}
|
||||
dense={true}
|
||||
virtualized={treeModel.visibleNodeCount > 50}
|
||||
/>
|
||||
) : (
|
||||
<div className="data-table-wrap">
|
||||
<table className="data-table min-w-[980px]">
|
||||
<thead>
|
||||
<table className="data-table financials-table min-w-[980px]">
|
||||
<thead className="sticky top-0 z-20 bg-[color:var(--panel)]">
|
||||
<tr>
|
||||
<th className="sticky left-0 z-10 bg-[color:var(--panel)]">
|
||||
Metric
|
||||
</th>
|
||||
{periods.map((period) => (
|
||||
<th className="bg-[color:var(--panel)]">Metric</th>
|
||||
{visiblePeriods.map((period) => (
|
||||
<th key={period.id}>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span>
|
||||
@@ -1136,7 +1236,7 @@ function FinancialsPageContent() {
|
||||
{group.label ? (
|
||||
<tr className="bg-[color:var(--panel-soft)]">
|
||||
<td
|
||||
colSpan={periods.length + 1}
|
||||
colSpan={visiblePeriods.length + 1}
|
||||
className="font-semibold text-[color:var(--terminal-bright)]"
|
||||
>
|
||||
{group.label}
|
||||
@@ -1146,14 +1246,9 @@ function FinancialsPageContent() {
|
||||
{group.rows.map((row) => (
|
||||
<tr
|
||||
key={row.key}
|
||||
className={
|
||||
selectedFlatRowKey === row.key
|
||||
? "bg-[color:var(--panel-soft)]"
|
||||
: undefined
|
||||
}
|
||||
onClick={() => setSelectedFlatRowKey(row.key)}
|
||||
>
|
||||
<td className="sticky left-0 z-10 bg-[color:var(--panel)]">
|
||||
<td className="bg-[color:var(--panel)]">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
@@ -1180,14 +1275,14 @@ function FinancialsPageContent() {
|
||||
) : null}
|
||||
</div>
|
||||
</td>
|
||||
{periods.map((period, index) => (
|
||||
{visiblePeriods.map((period, index) => (
|
||||
<td key={`${row.key}-${period.id}`}>
|
||||
{buildDisplayValue({
|
||||
row,
|
||||
periodId: period.id,
|
||||
previousPeriodId:
|
||||
index > 0
|
||||
? (periods[index - 1]?.id ?? null)
|
||||
? (visiblePeriods[index - 1]?.id ?? null)
|
||||
: null,
|
||||
commonSizeRow,
|
||||
displayMode,
|
||||
@@ -1210,207 +1305,235 @@ function FinancialsPageContent() {
|
||||
)}
|
||||
</Panel>
|
||||
|
||||
{isTreeStatementMode && isStatementSurfaceKind(surfaceKind) ? (
|
||||
<StatementRowInspector
|
||||
selection={selectedStatementRow}
|
||||
dimensionRows={dimensionRows}
|
||||
periods={periods}
|
||||
surfaceKind={surfaceKind}
|
||||
renderValue={renderStatementTreeCellValue}
|
||||
renderDimensionValue={renderDimensionValue}
|
||||
/>
|
||||
) : (
|
||||
<Panel title="Details" variant="flat" density="compact">
|
||||
{!selectedRow ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">
|
||||
Select a row to inspect details.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-4 text-sm">
|
||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-2.5 py-1.5">
|
||||
<p className="text-[color:var(--terminal-muted)]">Label</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">
|
||||
{selectedRow.label}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-2.5 py-1.5">
|
||||
<p className="text-[color:var(--terminal-muted)]">Key</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">
|
||||
{selectedRow.key}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isTaxonomyRow(selectedRow) ? (
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-2.5 py-1.5">
|
||||
<p className="text-[color:var(--terminal-muted)]">
|
||||
Taxonomy Concept
|
||||
</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">
|
||||
{selectedRow.qname}
|
||||
</p>
|
||||
</div>
|
||||
{financials ? (
|
||||
<CollapsibleSection
|
||||
title="Normalization Summary"
|
||||
subtitle="Row details, validation, and normalization diagnostics."
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<SummarySubsection title="Row Details">
|
||||
{isTreeStatementMode && isStatementSurfaceKind(surfaceKind) ? (
|
||||
<StatementRowInspector
|
||||
selection={selectedStatementRow}
|
||||
dimensionRows={dimensionRows}
|
||||
periods={visiblePeriods}
|
||||
surfaceKind={surfaceKind}
|
||||
renderValue={renderStatementTreeCellValue}
|
||||
renderDimensionValue={renderDimensionValue}
|
||||
showHeader={false}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-2.5 py-1.5">
|
||||
<p className="text-[color:var(--terminal-muted)]">
|
||||
Category
|
||||
<Panel variant="flat" density="compact">
|
||||
{!selectedRow ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">
|
||||
Select a row to inspect details.
|
||||
</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">
|
||||
{selectedRow.category}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-2.5 py-1.5">
|
||||
<p className="text-[color:var(--terminal-muted)]">Unit</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">
|
||||
{selectedRow.unit}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 text-sm">
|
||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-2.5 py-1.5">
|
||||
<p className="text-[color:var(--terminal-muted)]">
|
||||
Label
|
||||
</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">
|
||||
{selectedRow.label}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-2.5 py-1.5">
|
||||
<p className="text-[color:var(--terminal-muted)]">
|
||||
Key
|
||||
</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">
|
||||
{selectedRow.key}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isTaxonomyRow(selectedRow) ? (
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-2.5 py-1.5">
|
||||
<p className="text-[color:var(--terminal-muted)]">
|
||||
Taxonomy Concept
|
||||
</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">
|
||||
{selectedRow.qname}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-2.5 py-1.5">
|
||||
<p className="text-[color:var(--terminal-muted)]">
|
||||
Category
|
||||
</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">
|
||||
{selectedRow.category}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-2.5 py-1.5">
|
||||
<p className="text-[color:var(--terminal-muted)]">
|
||||
Unit
|
||||
</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">
|
||||
{selectedRow.unit}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isDerivedRow(selectedRow) ? (
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-2.5 py-1.5">
|
||||
<p className="text-[color:var(--terminal-muted)]">
|
||||
Source Row Keys
|
||||
</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">
|
||||
{(selectedRow.sourceRowKeys ?? []).join(", ") ||
|
||||
"n/a"}
|
||||
</p>
|
||||
<p className="mt-2 text-[color:var(--terminal-muted)]">
|
||||
Source Concepts
|
||||
</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">
|
||||
{(selectedRow.sourceConcepts ?? []).join(", ") ||
|
||||
"n/a"}
|
||||
</p>
|
||||
<p className="mt-2 text-[color:var(--terminal-muted)]">
|
||||
Source Fact IDs
|
||||
</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">
|
||||
{(selectedRow.sourceFactIds ?? []).join(", ") ||
|
||||
"n/a"}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!selectedRow ||
|
||||
!("hasDimensions" in selectedRow) ||
|
||||
!selectedRow.hasDimensions ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">
|
||||
No dimensional drill-down is available for this row.
|
||||
</p>
|
||||
) : dimensionRows.length === 0 ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">
|
||||
No dimensional facts were returned for the selected
|
||||
row.
|
||||
</p>
|
||||
) : (
|
||||
<div className="data-table-wrap">
|
||||
<table className="data-table min-w-[760px]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Period</th>
|
||||
<th>Axis</th>
|
||||
<th>Member</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dimensionRows.map((row, index) => (
|
||||
<tr
|
||||
key={`${row.rowKey}-${row.periodId}-${row.axis}-${row.member}-${index}`}
|
||||
>
|
||||
<td>
|
||||
{visiblePeriods.find(
|
||||
(period) => period.id === row.periodId,
|
||||
)?.periodLabel ?? row.periodId}
|
||||
</td>
|
||||
<td>{row.axis}</td>
|
||||
<td>{row.member}</td>
|
||||
<td>
|
||||
{formatMetricValue({
|
||||
value: row.value,
|
||||
unit: isTaxonomyRow(selectedRow)
|
||||
? "currency"
|
||||
: selectedRow.unit,
|
||||
scale: valueScale,
|
||||
rowKey: selectedRow.key,
|
||||
surfaceKind,
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
)}
|
||||
</SummarySubsection>
|
||||
|
||||
{isDerivedRow(selectedRow) ? (
|
||||
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-2.5 py-1.5">
|
||||
<p className="text-[color:var(--terminal-muted)]">
|
||||
Source Row Keys
|
||||
</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">
|
||||
{(selectedRow.sourceRowKeys ?? []).join(", ") || "n/a"}
|
||||
</p>
|
||||
<p className="mt-2 text-[color:var(--terminal-muted)]">
|
||||
Source Concepts
|
||||
</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">
|
||||
{(selectedRow.sourceConcepts ?? []).join(", ") || "n/a"}
|
||||
</p>
|
||||
<p className="mt-2 text-[color:var(--terminal-muted)]">
|
||||
Source Fact IDs
|
||||
</p>
|
||||
<p className="font-semibold text-[color:var(--terminal-bright)]">
|
||||
{(selectedRow.sourceFactIds ?? []).join(", ") || "n/a"}
|
||||
</p>
|
||||
{(surfaceKind === "income_statement" ||
|
||||
surfaceKind === "balance_sheet" ||
|
||||
surfaceKind === "cash_flow_statement") && (
|
||||
<SummarySubsection title="Validation">
|
||||
<div className="mb-3 flex items-center gap-2 text-sm text-[color:var(--terminal-muted)]">
|
||||
<AlertTriangle className="size-4" />
|
||||
<span>
|
||||
Overall status:{" "}
|
||||
{financials.metrics.validation?.status ?? "not_run"}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!selectedRow ||
|
||||
!("hasDimensions" in selectedRow) ||
|
||||
!selectedRow.hasDimensions ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">
|
||||
No dimensional drill-down is available for this row.
|
||||
</p>
|
||||
) : dimensionRows.length === 0 ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">
|
||||
No dimensional facts were returned for the selected row.
|
||||
</p>
|
||||
) : (
|
||||
<div className="data-table-wrap">
|
||||
<table className="data-table min-w-[760px]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Period</th>
|
||||
<th>Axis</th>
|
||||
<th>Member</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dimensionRows.map((row, index) => (
|
||||
<tr
|
||||
key={`${row.rowKey}-${row.periodId}-${row.axis}-${row.member}-${index}`}
|
||||
>
|
||||
<td>
|
||||
{periods.find(
|
||||
(period) => period.id === row.periodId,
|
||||
)?.periodLabel ?? row.periodId}
|
||||
</td>
|
||||
<td>{row.axis}</td>
|
||||
<td>{row.member}</td>
|
||||
<td>
|
||||
{formatMetricValue({
|
||||
value: row.value,
|
||||
unit: isTaxonomyRow(selectedRow)
|
||||
? "currency"
|
||||
: selectedRow.unit,
|
||||
scale: valueScale,
|
||||
rowKey: selectedRow.key,
|
||||
surfaceKind,
|
||||
})}
|
||||
</td>
|
||||
{(financials.metrics.validation?.checks.length ?? 0) === 0 ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">
|
||||
No validation checks available yet.
|
||||
</p>
|
||||
) : (
|
||||
<div className="data-table-wrap">
|
||||
<table className="data-table min-w-[760px]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Metric</th>
|
||||
<th>Taxonomy</th>
|
||||
<th>LLM (PDF)</th>
|
||||
<th>Status</th>
|
||||
<th>Pages</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{financials.metrics.validation?.checks.map((check) => (
|
||||
<tr key={check.metricKey}>
|
||||
<td>{check.metricKey}</td>
|
||||
<td>
|
||||
{formatMetricValue({
|
||||
value: check.taxonomyValue,
|
||||
unit: "currency",
|
||||
scale: valueScale,
|
||||
rowKey: check.metricKey,
|
||||
surfaceKind,
|
||||
})}
|
||||
</td>
|
||||
<td>
|
||||
{formatMetricValue({
|
||||
value: check.llmValue,
|
||||
unit: "currency",
|
||||
scale: valueScale,
|
||||
rowKey: check.metricKey,
|
||||
surfaceKind,
|
||||
})}
|
||||
</td>
|
||||
<td>{check.status}</td>
|
||||
<td>
|
||||
{(check.evidencePages ?? []).join(", ") || "n/a"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</SummarySubsection>
|
||||
)}
|
||||
|
||||
{(surfaceKind === "income_statement" ||
|
||||
surfaceKind === "balance_sheet" ||
|
||||
surfaceKind === "cash_flow_statement") &&
|
||||
financials ? (
|
||||
<Panel title="Validation" variant="flat" density="compact">
|
||||
<div className="mb-3 flex items-center gap-2 text-sm text-[color:var(--terminal-muted)]">
|
||||
<AlertTriangle className="size-4" />
|
||||
<span>
|
||||
Overall status:{" "}
|
||||
{financials.metrics.validation?.status ?? "not_run"}
|
||||
</span>
|
||||
{isTreeStatementMode ? (
|
||||
<SummarySubsection title="Normalization">
|
||||
<NormalizationSummary
|
||||
normalization={financials.normalization}
|
||||
showHeader={false}
|
||||
/>
|
||||
</SummarySubsection>
|
||||
) : null}
|
||||
</div>
|
||||
{(financials.metrics.validation?.checks.length ?? 0) === 0 ? (
|
||||
<p className="text-sm text-[color:var(--terminal-muted)]">
|
||||
No validation checks available yet.
|
||||
</p>
|
||||
) : (
|
||||
<div className="data-table-wrap">
|
||||
<table className="data-table min-w-[760px]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Metric</th>
|
||||
<th>Taxonomy</th>
|
||||
<th>LLM (PDF)</th>
|
||||
<th>Status</th>
|
||||
<th>Pages</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{financials.metrics.validation?.checks.map((check) => (
|
||||
<tr key={check.metricKey}>
|
||||
<td>{check.metricKey}</td>
|
||||
<td>
|
||||
{formatMetricValue({
|
||||
value: check.taxonomyValue,
|
||||
unit: "currency",
|
||||
scale: valueScale,
|
||||
rowKey: check.metricKey,
|
||||
surfaceKind,
|
||||
})}
|
||||
</td>
|
||||
<td>
|
||||
{formatMetricValue({
|
||||
value: check.llmValue,
|
||||
unit: "currency",
|
||||
scale: valueScale,
|
||||
rowKey: check.metricKey,
|
||||
surfaceKind,
|
||||
})}
|
||||
</td>
|
||||
<td>{check.status}</td>
|
||||
<td>{(check.evidencePages ?? []).join(", ") || "n/a"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
) : null}
|
||||
|
||||
{financials && isTreeStatementMode ? (
|
||||
<NormalizationSummary normalization={financials.normalization} />
|
||||
</CollapsibleSection>
|
||||
) : null}
|
||||
</AppShell>
|
||||
);
|
||||
|
||||
236
app/globals.css
236
app/globals.css
@@ -1,25 +1,23 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--font-display:
|
||||
"Avenir Next", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||
--font-mono:
|
||||
"Menlo", "SFMono-Regular", "Consolas", "Liberation Mono", monospace;
|
||||
--bg-0: #121417;
|
||||
--bg-1: #181b20;
|
||||
--bg-2: #21252b;
|
||||
--panel: rgba(28, 31, 36, 0.84);
|
||||
--panel-soft: rgba(36, 39, 45, 0.72);
|
||||
--panel-bright: rgba(49, 53, 60, 0.94);
|
||||
--line-weak: rgba(196, 202, 211, 0.18);
|
||||
--line-strong: rgba(220, 226, 234, 0.34);
|
||||
--accent: #d9dee5;
|
||||
--accent-strong: #f4f7fb;
|
||||
--danger: #ff8e8e;
|
||||
--danger-soft: rgba(111, 46, 46, 0.42);
|
||||
--terminal-bright: #f3f5f7;
|
||||
--terminal-muted: #a1a9b3;
|
||||
--focus-ring: rgba(229, 231, 235, 0.14);
|
||||
--font-display: system-ui, -apple-system, sans-serif;
|
||||
--font-mono: "SF Mono", "Fira Code", monospace;
|
||||
--bg-0: #0f0f0f;
|
||||
--bg-1: #161616;
|
||||
--bg-2: #1c1c1c;
|
||||
--panel: #1a1a1a;
|
||||
--panel-soft: #222222;
|
||||
--panel-bright: #2a2a2a;
|
||||
--line-weak: rgba(255, 255, 255, 0.08);
|
||||
--line-strong: rgba(255, 255, 255, 0.16);
|
||||
--accent: #e0e0e0;
|
||||
--accent-strong: #ffffff;
|
||||
--danger: #ff6b6b;
|
||||
--danger-soft: rgba(255, 107, 107, 0.15);
|
||||
--terminal-bright: #f0f0f0;
|
||||
--terminal-muted: #888888;
|
||||
--focus-ring: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -58,18 +56,7 @@ body {
|
||||
overflow-x: hidden;
|
||||
font-family: var(--font-display), sans-serif;
|
||||
color: var(--terminal-bright);
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 18% -10%,
|
||||
rgba(170, 178, 188, 0.16),
|
||||
transparent 35%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 84% 0%,
|
||||
rgba(121, 128, 138, 0.14),
|
||||
transparent 30%
|
||||
),
|
||||
linear-gradient(140deg, var(--bg-0), var(--bg-1) 50%, var(--bg-2));
|
||||
background: var(--bg-0);
|
||||
}
|
||||
|
||||
.app-surface,
|
||||
@@ -79,36 +66,12 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ambient-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(204, 210, 218, 0.06) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(204, 210, 218, 0.05) 1px, transparent 1px);
|
||||
background-size: 34px 34px;
|
||||
mask-image: radial-gradient(ellipse at center, black 20%, transparent 75%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.noise-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0.24;
|
||||
background-image: radial-gradient(
|
||||
rgba(220, 226, 234, 0.1) 0.7px,
|
||||
transparent 0.7px
|
||||
);
|
||||
background-size: 4px 4px;
|
||||
}
|
||||
|
||||
.terminal-caption {
|
||||
font-family: var(--font-mono), monospace;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
font-family: var(--font-mono), monospace;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
a,
|
||||
@@ -126,23 +89,15 @@ textarea {
|
||||
|
||||
.data-surface {
|
||||
border: 1px solid var(--line-weak);
|
||||
border-radius: 1rem;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(40, 43, 49, 0.92),
|
||||
rgba(24, 27, 32, 0.78)
|
||||
);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.data-table-wrap {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--line-weak);
|
||||
border-radius: 1rem;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(34, 37, 42, 0.9),
|
||||
rgba(20, 23, 27, 0.76)
|
||||
);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
@@ -163,54 +118,31 @@ textarea {
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background-color: rgba(63, 68, 76, 0.32);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.ambient-grid {
|
||||
animation: subtle-grid-shift 18s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes subtle-grid-shift {
|
||||
0% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.ambient-grid {
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
background-color: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
body {
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 24% -4%,
|
||||
rgba(170, 178, 188, 0.14),
|
||||
transparent 36%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 82% 2%,
|
||||
rgba(121, 128, 138, 0.12),
|
||||
transparent 30%
|
||||
),
|
||||
linear-gradient(155deg, var(--bg-0), var(--bg-1) 54%, var(--bg-2));
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: 0.65rem 0.55rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.financial-matrix-metric-col {
|
||||
width: 18rem;
|
||||
}
|
||||
|
||||
.financial-matrix-period-col {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.financial-matrix-header,
|
||||
.financial-matrix-section-cell,
|
||||
.financial-matrix-sticky-cell,
|
||||
.financial-matrix-value-cell {
|
||||
padding-left: 0.65rem;
|
||||
padding-right: 0.65rem;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-compact {
|
||||
@@ -244,7 +176,87 @@ textarea {
|
||||
}
|
||||
|
||||
.data-table-dense tbody tr:hover {
|
||||
background-color: rgba(63, 68, 76, 0.32);
|
||||
background-color: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.financials-table tbody tr:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.financial-matrix-wrap {
|
||||
overflow: auto;
|
||||
border: 1px solid var(--line-weak);
|
||||
border-radius: 0.75rem;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.financial-matrix {
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.financial-matrix-metric-col {
|
||||
width: 24rem;
|
||||
}
|
||||
|
||||
.financial-matrix-period-col {
|
||||
width: 9rem;
|
||||
}
|
||||
|
||||
.financial-matrix-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
padding: 0.85rem 0.9rem;
|
||||
border-bottom: 1px solid var(--line-strong);
|
||||
background: color-mix(in srgb, var(--panel) 92%, black 8%);
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-mono), monospace;
|
||||
}
|
||||
|
||||
.financial-matrix-section-row td {
|
||||
border-bottom: 1px solid var(--line-strong);
|
||||
}
|
||||
|
||||
.financial-matrix-section-cell {
|
||||
padding: 0.7rem 0.9rem;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--terminal-muted);
|
||||
}
|
||||
|
||||
.financial-matrix-row-detail td {
|
||||
background: rgba(255, 255, 255, 0.015);
|
||||
}
|
||||
|
||||
.financial-matrix-sticky-cell,
|
||||
.financial-matrix-value-cell {
|
||||
padding: 0.45rem 0.9rem;
|
||||
border-bottom: 1px solid var(--line-weak);
|
||||
white-space: nowrap;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.financial-matrix-sticky-cell {
|
||||
border-right: 1px solid var(--line-weak);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.financial-matrix-sticky-cell-detail {
|
||||
background: color-mix(in srgb, var(--panel) 92%, white 8%);
|
||||
}
|
||||
|
||||
.financial-matrix-value-cell {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--terminal-bright);
|
||||
}
|
||||
|
||||
.metric-compact {
|
||||
@@ -382,12 +394,8 @@ textarea {
|
||||
.screener-table-wrap {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--line-weak);
|
||||
border-radius: 0.75rem;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(34, 37, 42, 0.9),
|
||||
rgba(20, 23, 27, 0.76)
|
||||
);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.screener-table-wrap thead {
|
||||
|
||||
Reference in New Issue
Block a user