diff --git a/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx b/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx index 30acb999a..deba5cb66 100644 --- a/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx +++ b/apps/apollo-vertex/app/preview/dashboard-minimal/page.tsx @@ -3,7 +3,10 @@ import dynamic from "next/dynamic"; const DashboardTemplate = dynamic( - () => import("@/templates/dashboard/DashboardTemplate").then((mod) => mod.DashboardTemplate), + () => + import("@/templates/dashboard/DashboardTemplate").then( + (mod) => mod.DashboardTemplate, + ), { ssr: false }, ); diff --git a/apps/apollo-vertex/app/preview/dashboard/page.tsx b/apps/apollo-vertex/app/preview/dashboard/page.tsx index db71ea817..018db5cc2 100644 --- a/apps/apollo-vertex/app/preview/dashboard/page.tsx +++ b/apps/apollo-vertex/app/preview/dashboard/page.tsx @@ -3,7 +3,10 @@ import dynamic from "next/dynamic"; const DashboardTemplate = dynamic( - () => import("@/templates/dashboard/DashboardTemplate").then((mod) => mod.DashboardTemplate), + () => + import("@/templates/dashboard/DashboardTemplate").then( + (mod) => mod.DashboardTemplate, + ), { ssr: false }, ); diff --git a/apps/apollo-vertex/templates/dashboard/AutopilotInsight.tsx b/apps/apollo-vertex/templates/dashboard/AutopilotInsight.tsx new file mode 100644 index 000000000..4f98b2323 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/AutopilotInsight.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +interface AutopilotInsightProps { + onClose: () => void; + sourceCardTitle: string; +} + +export function AutopilotInsight({ + onClose, + sourceCardTitle, +}: AutopilotInsightProps) { + return ( + + {/* Close button */} + + + +
+ Autopilot + Autopilot + + Autopilot Insight + +
+

+ Analyzing {sourceCardTitle} +

+
+ + + {/* Placeholder for future chat UX responses */} +
+
+

+ Autopilot response area +

+

+ Chat UX content will appear here +

+
+
+
+
+ ); +} diff --git a/apps/apollo-vertex/templates/dashboard/DashboardCards.tsx b/apps/apollo-vertex/templates/dashboard/DashboardCards.tsx new file mode 100644 index 000000000..3a6ad30aa --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/DashboardCards.tsx @@ -0,0 +1,288 @@ +import { AlertTriangle, CheckCircle, Clock, XCircle } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +// --- Types --- + +export interface KpiItem { + label: string; + value: string; + icon: LucideIcon; + change: string; +} + +// --- Sample data --- + +const invoices = [ + { + id: "INV-4021", + vendor: "Acme Corp", + amount: "$12,450.00", + status: "Processed" as const, + date: "Mar 18, 2026", + }, + { + id: "INV-4020", + vendor: "Global Supplies Ltd", + amount: "$3,280.50", + status: "Pending" as const, + date: "Mar 18, 2026", + }, + { + id: "INV-4019", + vendor: "TechParts Inc", + amount: "$8,920.00", + status: "In Review" as const, + date: "Mar 17, 2026", + }, + { + id: "INV-4018", + vendor: "Office Depot", + amount: "$1,150.75", + status: "Processed" as const, + date: "Mar 17, 2026", + }, + { + id: "INV-4017", + vendor: "CloudServ Solutions", + amount: "$24,000.00", + status: "Failed" as const, + date: "Mar 16, 2026", + }, + { + id: "INV-4016", + vendor: "Metro Logistics", + amount: "$6,780.00", + status: "Processed" as const, + date: "Mar 16, 2026", + }, +]; + +const statusVariant: Record< + string, + "default" | "secondary" | "destructive" | "outline" +> = { + Processed: "default", + Pending: "secondary", + Failed: "destructive", + "In Review": "outline", +}; + +const statusIcon: Record = { + Processed: CheckCircle, + Pending: Clock, + Failed: XCircle, + "In Review": AlertTriangle, +}; + +const activityBars = [ + { label: "Mon", height: 60 }, + { label: "Tue", height: 85 }, + { label: "Wed", height: 45 }, + { label: "Thu", height: 92 }, + { label: "Fri", height: 78 }, + { label: "Sat", height: 30 }, + { label: "Sun", height: 15 }, +]; + +const recentActivity = [ + { text: "INV-4021 processed successfully", time: "2 min ago" }, + { text: "INV-4020 submitted for review", time: "15 min ago" }, + { text: "Batch processing completed (42 invoices)", time: "1 hr ago" }, + { text: "INV-4017 failed — missing PO number", time: "3 hrs ago" }, +]; + +const pipelineStages = [ + { label: "OCR Extraction", value: 96 }, + { label: "Field Validation", value: 88 }, + { label: "Approval Routing", value: 72 }, + { label: "Final Review", value: 64 }, +]; + +const complianceChecks = [ + { label: "Income Verification", pass: 98 }, + { label: "Credit Score Threshold", pass: 96 }, + { label: "Debt-to-Income Ratio", pass: 91 }, + { label: "Collateral Appraisal", pass: 87 }, + { label: "Document Completeness", pass: 94 }, +]; + +// --- Card components --- + +export function KpiCards({ kpis }: { kpis: KpiItem[] }) { + return ( + <> + {kpis.map((kpi) => ( + + +
+ + {kpi.label} + + +
+
+ +
{kpi.value}
+

+ {kpi.change} from last + week +

+
+
+ ))} + + ); +} + +export function InvoiceTable() { + return ( + + + Recent Invoices + + + + + + Invoice + Vendor + Amount + Status + Date + + + + {invoices.map((inv) => { + const StatusIcon = statusIcon[inv.status]; + return ( + + {inv.id} + {inv.vendor} + {inv.amount} + + + + {inv.status} + + + + {inv.date} + + + ); + })} + +
+
+
+ ); +} + +export function ActivityBarChart() { + return ( + + + Processing Activity + + +
+ {activityBars.map((bar) => ( +
+
+ {bar.label} +
+ ))} +
+ + + ); +} + +export function ActivityFeed() { + return ( + + + Recent Activity + + +
+ {recentActivity.map((event) => ( +
+
+
+

{event.text}

+

{event.time}

+
+
+ ))} +
+ + + ); +} + +export function PipelineProgress() { + return ( + + + Processing Pipeline + + +
+ {pipelineStages.map((stage) => ( +
+
+ {stage.label} + {stage.value}% +
+ +
+ ))} +
+
+
+ ); +} + +export function ComplianceProgress() { + return ( + + + Compliance Pass Rates + + +
+ {complianceChecks.map((check) => ( +
+
+ {check.label} + {check.pass}% +
+ +
+ ))} +
+
+
+ ); +} diff --git a/apps/apollo-vertex/templates/dashboard/DashboardTemplate.tsx b/apps/apollo-vertex/templates/dashboard/DashboardTemplate.tsx index 5e2d6ae00..b4b3dd48f 100644 --- a/apps/apollo-vertex/templates/dashboard/DashboardTemplate.tsx +++ b/apps/apollo-vertex/templates/dashboard/DashboardTemplate.tsx @@ -43,15 +43,24 @@ const routeTree = dashboardRootRoute.addChildren([ ]), ]); +const BASE_PATHS = { + default: "/preview/dashboard/", + minimal: "/preview/dashboard-minimal/", +} as const; + +function normalizeEntry(path: string): string { + if (path === "/preview/dashboard") return BASE_PATHS.default; + if (path === "/preview/dashboard-minimal") return BASE_PATHS.minimal; + return path; +} + function getInitialEntry( storageKey: DashboardPreviewPathKey, variant?: "minimal", ) { const stored = localStorage.getItem(storageKey); - if (stored) return stored; - return variant === "minimal" - ? "/preview/dashboard-minimal" - : "/preview/dashboard"; + if (stored) return normalizeEntry(stored); + return variant === "minimal" ? BASE_PATHS.minimal : BASE_PATHS.default; } function createDashboardRouter( @@ -75,7 +84,7 @@ export function DashboardTemplate({ shellVariant }: DashboardTemplateProps) { useEffect(() => { const unsubscribe = router.subscribe("onResolved", ({ toLocation }) => { - localStorage.setItem(storageKey, toLocation.pathname); + localStorage.setItem(storageKey, normalizeEntry(toLocation.pathname)); }); return unsubscribe; }, [router, storageKey]); diff --git a/apps/apollo-vertex/templates/dashboard/InsightCardInner.tsx b/apps/apollo-vertex/templates/dashboard/InsightCardInner.tsx new file mode 100644 index 000000000..124ad0fcb --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/InsightCardInner.tsx @@ -0,0 +1,290 @@ +"use client"; + +import { ArrowUpRight, Maximize2, Minimize2 } from "lucide-react"; +import { + Card, + CardAction, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + cardBgStyle, + getInsightCardClasses, + type CardConfig, + type InsightCardConfig, +} from "./glow-config"; +import { InsightCardBody } from "./insight-card-renderers"; +import { useDashboardData } from "./dashboard-data-context"; +import { type DrilldownTab, drilldownTabs } from "./drilldown-tabs"; +import { + DrilldownTabContent, + AutopilotPrompts, +} from "./ExpandedInsightContent"; +import type { ExpandPhase } from "./InsightGrid"; + +interface InsightCardInnerProps { + cfg: InsightCardConfig; + cardIndex: number; + shared: string; + cards: CardConfig; + isExpanding: boolean; + isThis: boolean; + phase: ExpandPhase; + viewMode: "desktop" | "compact" | "stacked"; + drilldownTab: DrilldownTab; + onDrilldownTabChange: (tab: DrilldownTab) => void; + onExpandClick: () => void; + onAutopilotOpen?: (() => void) | null; + isAutopilotActive?: boolean; + className?: string; + style?: React.CSSProperties; +} + +export function InsightCardInner({ + cfg, + cardIndex, + shared, + cards, + isExpanding, + isThis, + phase, + viewMode, + onExpandClick, + onAutopilotOpen, + drilldownTab, + onDrilldownTabChange, + isAutopilotActive = false, + className = "", + style, +}: InsightCardInnerProps) { + const { data } = useDashboardData(); + const cardTitle = data.insightCards[cardIndex]?.title ?? cfg.content.title; + const hasDrilldown = cfg.content.chartType === "horizontal-bars"; + const isExpandedWithDrilldown = + isThis && isExpanding && hasDrilldown && phase === "full"; + const classes = getInsightCardClasses(cfg.content, viewMode); + const isInteractive = cfg.interaction !== "static"; + + return ( + + + + {cardTitle} + + {isInteractive && cfg.interaction === "expand" && ( + +
+ {onAutopilotOpen && ( + + )} + +
+
+ )} + {isInteractive && cfg.interaction === "navigate" && !isThis && ( + + + + )} + {/* Drilldown tabs — below title when expanded */} + {isThis && + isExpanding && + hasDrilldown && + (phase === "height" || phase === "full") && + (() => { + const visibleTabs = drilldownTabs.slice(0, 4); + const overflowTabs = drilldownTabs.slice(4); + const isOverflowActive = overflowTabs.some( + (t) => t.key === drilldownTab, + ); + return ( +
+ {visibleTabs.map((tab) => ( + + ))} + {overflowTabs.length > 0 && ( +
+ + + + +
+ )} +
+ ); + })()} +
+ {isExpandedWithDrilldown ? ( + /* Expanded with drilldown — unified layout for all tabs */ +
+
+
+ {drilldownTab === "overview" ? ( + + ) : ( + + )} +
+
+
+ onAutopilotOpen?.()} /> +
+
+ ) : ( + /* Default card content — not expanded or no drilldown */ + + + + )} + {/* Non-drilldown expanded content (other card types) */} + {isThis && + isExpanding && + !hasDrilldown && + (phase === "height" || phase === "full") && ( +
+ {phase === "full" ? ( +
+ + Additional content + +
+ ) : ( +
+
+
+
+
+ )} +
+ )} + + ); +} + +export type { InsightCardInnerProps }; diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx new file mode 100644 index 000000000..496696b68 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx @@ -0,0 +1,240 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useDashboardData } from "./dashboard-data-context"; +import type { DrilldownTab } from "./drilldown-tabs"; +import { + AutopilotPrompts, + DrilldownTabContent, +} from "./ExpandedInsightContent"; +import type { CardConfig, LayoutConfig } from "./glow-config"; +import { InsightCardInner } from "./InsightCardInner"; + +export type ExpandPhase = "idle" | "width" | "height" | "full"; + +const sizeToFr: Record = { sm: "1fr", md: "2fr", lg: "1fr" }; + +export function InsightGrid({ + layout, + shared, + cards, + viewMode = "desktop", + onAutopilotOpen, + autopilotActiveIdx, +}: { + layout: LayoutConfig; + shared: string; + cards: CardConfig; + viewMode?: "desktop" | "compact" | "stacked"; + onAutopilotOpen?: (sourceTitle: string, idx: number) => void; + autopilotActiveIdx?: number | null; +}) { + const { data } = useDashboardData(); + const [expandedIdx, setExpandedIdx] = useState(null); + const [phase, setPhase] = useState("idle"); + const [drilldownTab, setDrilldownTab] = useState("overview"); + + useEffect(() => { + if (expandedIdx === null) { + setPhase("idle"); + setDrilldownTab("overview"); + return; + } + requestAnimationFrame(() => setPhase("width")); + const t1 = setTimeout(() => setPhase("height"), 300); + const t2 = setTimeout(() => setPhase("full"), 600); + return () => { + clearTimeout(t1); + clearTimeout(t2); + }; + }, [expandedIdx]); + + const visibleCards = layout.insightCards + .map((cfg, i) => { + const dataCard = data.insightCards[i]; + const merged = dataCard + ? { + ...cfg, + size: dataCard.size ?? cfg.size, + interaction: dataCard.interaction ?? cfg.interaction, + content: { + ...cfg.content, + title: dataCard.title ?? cfg.content.title, + }, + } + : cfg; + return { cfg: merged, idx: i }; + }) + .filter(({ cfg }) => cfg.visible); + const rows: (typeof visibleCards)[] = []; + for (let i = 0; i < visibleCards.length; i += 2) { + rows.push(visibleCards.slice(i, i + 2)); + } + + const isExpanding = expandedIdx !== null; + const expandedRow = isExpanding + ? rows.findIndex((row) => row.some(({ idx }) => idx === expandedIdx)) + : -1; + + const handleClick = ( + cfg: (typeof visibleCards)[number]["cfg"], + idx: number, + ) => { + if (cfg.interaction === "expand") { + setExpandedIdx(expandedIdx === idx ? null : idx); + } + }; + + // Build grid-template-rows + let rowTemplates: string[]; + if (viewMode === "compact") { + rowTemplates = visibleCards.map(({ idx }) => { + if (!isExpanding) return "1fr"; + if (idx === expandedIdx) return "1fr"; + if (phase !== "idle") return "0fr"; + return "1fr"; + }); + } else { + rowTemplates = rows.map((_, rowIndex) => { + const isOtherRow = isExpanding && rowIndex !== expandedRow; + if (isOtherRow && (phase === "height" || phase === "full")) return "0fr"; + return "1fr"; + }); + } + + const sharedProps = { + shared, + cards, + isExpanding, + phase, + viewMode, + drilldownTab, + onDrilldownTabChange: setDrilldownTab, + }; + + return ( +
+ {viewMode === "compact" + ? visibleCards.map(({ cfg, idx }) => { + const isThis = idx === expandedIdx; + const isOther = isExpanding && !isThis; + return ( +
+ handleClick(cfg, idx)} + onAutopilotOpen={ + onAutopilotOpen + ? () => + onAutopilotOpen( + data.insightCards[idx]?.title ?? cfg.content.title, + idx, + ) + : null + } + isAutopilotActive={autopilotActiveIdx === idx} + className="h-full" + /> +
+ ); + }) + : rows.map((row, rowIndex) => { + const isRowWithExpanded = rowIndex === expandedRow; + const isOtherRow = isExpanding && !isRowWithExpanded; + const cols = row + .map(({ cfg, idx }) => { + if (!isExpanding) + return cfg.size === "lg" ? "1fr" : sizeToFr[cfg.size]; + if (idx === expandedIdx) + return phase === "idle" + ? cfg.size === "lg" + ? "1fr" + : sizeToFr[cfg.size] + : "1fr"; + if (isRowWithExpanded) + return phase === "idle" + ? cfg.size === "lg" + ? "1fr" + : sizeToFr[cfg.size] + : "0fr"; + return cfg.size === "lg" ? "1fr" : sizeToFr[cfg.size]; + }) + .join(" "); + return ( +
idx).join("-")} + className="grid transition-all duration-300 ease-in-out overflow-hidden min-h-0" + style={ + { + gridTemplateColumns: cols, + gap: isRowWithExpanded && phase !== "idle" ? 0 : layout.gap, + opacity: + isOtherRow && (phase === "height" || phase === "full") + ? 0 + : 1, + } as React.CSSProperties + } + > + {row.map(({ cfg, idx }) => { + const isThis = idx === expandedIdx; + const isSibling = isExpanding && !isThis && isRowWithExpanded; + return ( + handleClick(cfg, idx)} + onAutopilotOpen={ + onAutopilotOpen + ? () => + onAutopilotOpen( + data.insightCards[idx]?.title ?? + cfg.content.title, + idx, + ) + : null + } + isAutopilotActive={autopilotActiveIdx === idx} + style={{ + opacity: isSibling && phase !== "idle" ? 0 : 1, + transform: + isSibling && phase !== "idle" + ? "scale(0.95)" + : "scale(1)", + }} + /> + ); + })} +
+ ); + })} +
+ ); +} + +// Re-export for consumers that import these from InsightGrid +export { DrilldownTabContent, AutopilotPrompts }; +export type { DrilldownTab }; diff --git a/apps/apollo-vertex/templates/dashboard/PromptBar.tsx b/apps/apollo-vertex/templates/dashboard/PromptBar.tsx new file mode 100644 index 000000000..1df6d34be --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/PromptBar.tsx @@ -0,0 +1,173 @@ +"use client"; + +import { useState } from "react"; +import { MessagesSquare, Minimize2 } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { cardBgStyle, type CardConfig } from "./glow-config"; +import { useDashboardData } from "./dashboard-data-context"; + +export function PromptBar({ + shared, + cards, + isExpanded = false, + onSubmit, + onExpand, + onCollapse, +}: { + shared: string; + cards: CardConfig; + isExpanded?: boolean; + onSubmit?: (query: string) => void; + onExpand?: () => void; + onCollapse?: () => void; +}) { + const { data } = useDashboardData(); + const [value, setValue] = useState(""); + const hasInput = value.trim().length > 0; + + const handleSubmit = () => { + if (hasInput && onSubmit) { + onSubmit(value); + } + }; + + const handleChipClick = (suggestion: string) => { + setValue(suggestion); + onSubmit?.(suggestion); + }; + + return ( +
+ {/* Expanded response area */} + {isExpanded && ( +
+
+
+ Autopilot + Autopilot + + Autopilot + +
+ {onCollapse && ( + + )} +
+
+

+ Responses will appear here +

+
+
+
+ )} + {/* Suggestion badges — hidden when expanded */} + {!isExpanded && ( +
+
+
+ + handleChipClick( + data.promptSuggestions[0] ?? "Show me top risk factors", + ) + } + > + {data.promptSuggestions[0] ?? "Show me top risk factors"} + + + handleChipClick( + data.promptSuggestions[1] ?? "Compare Q1 vs Q2 performance", + ) + } + > + {data.promptSuggestions[1] ?? "Compare Q1 vs Q2 performance"} + +
+
+
+ )} + {/* Input bar */} +
+ setValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleSubmit(); + }} + placeholder={data.promptPlaceholder} + className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" + /> +
+ + +
+
+
+ ); +} diff --git a/apps/apollo-vertex/templates/dashboard/chart-stubs.tsx b/apps/apollo-vertex/templates/dashboard/chart-stubs.tsx new file mode 100644 index 000000000..ee56edb34 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/chart-stubs.tsx @@ -0,0 +1,94 @@ +"use client"; + +const sparklinePoints = [4, 7, 5, 9, 6, 8, 12, 10, 14, 11, 15, 13]; +const areaPoints = [3, 5, 4, 8, 6, 9, 7, 11, 10, 14, 12, 16]; + +export function DonutContent() { + return ( +
+
+ + + + +
+ + 47% + + + funded + +
+
+
+ ); +} + +export function SparklineContent() { + const max = Math.max(...sparklinePoints); + const h = 40; + const w = 120; + const step = w / (sparklinePoints.length - 1); + const points = sparklinePoints + .map((v, i) => `${i * step},${h - (v / max) * h}`) + .join(" "); + + return ( +
+ + + +
+ ); +} + +export function AreaContent() { + const max = Math.max(...areaPoints); + const h = 40; + const w = 120; + const step = w / (areaPoints.length - 1); + const linePoints = areaPoints + .map((v, i) => `${i * step},${h - (v / max) * h}`) + .join(" "); + const areaPath = `0,${h} ${linePoints} ${w},${h}`; + + return ( +
+ + + + +
+ ); +} diff --git a/apps/apollo-vertex/templates/dashboard/drilldown-tabs.ts b/apps/apollo-vertex/templates/dashboard/drilldown-tabs.ts new file mode 100644 index 000000000..536b1f220 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/drilldown-tabs.ts @@ -0,0 +1,13 @@ +export type DrilldownTab = + | "overview" + | "trend" + | "categories" + | "products" + | "actions"; + +export const drilldownTabs: { key: DrilldownTab; label: string }[] = [ + { key: "overview", label: "Overview" }, + { key: "categories", label: "Categories" }, + { key: "products", label: "Products" }, + { key: "actions", label: "Actions" }, +];