diff --git a/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx b/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx new file mode 100644 index 000000000..e3321b5cb --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/ExpandedInsightContent.tsx @@ -0,0 +1,338 @@ +"use client"; + +import { useState } from "react"; +import { Badge } from "@/components/ui/badge"; + +// --- Sample data --- + +const trendData = { + weeks: ["W1", "W2", "W3", "W4", "W5", "W6", "W7", "W8"], + series: [ + { + label: "Wrong size/fit", + color: "bg-chart-1", + stroke: "stroke-chart-1", + values: [31, 33, 34, 35, 36, 37, 39, 41], + }, + { + label: "Damaged in transit", + color: "bg-chart-2", + stroke: "stroke-chart-2", + values: [25, 24, 26, 23, 22, 24, 23, 21], + }, + { + label: "Not as described", + color: "bg-chart-3", + stroke: "stroke-chart-3", + values: [20, 19, 18, 19, 18, 17, 18, 17], + }, + ], + takeaway: + "Fit-related returns have grown steadily over 8 weeks (+32%), while damage and description issues remain flat.", +}; + +const categoryBreakdown = [ + { category: "Women's Apparel", pct: 48, highlight: true }, + { category: "Footwear", pct: 27 }, + { category: "Men's Apparel", pct: 16 }, + { category: "Accessories", pct: 9 }, +]; +const categoryInsight = + "Women's apparel and footwear account for 75% of all fit-related returns. Sizing inconsistency across brands is the primary driver."; + +const topProducts = [ + { + name: "Slim Fit Chinos — Navy", + returnRate: 18.4, + issue: "Wrong size", + impact: "$12,400", + }, + { + name: "Running Shoe Pro V2", + returnRate: 15.2, + issue: "Wrong fit", + impact: "$9,800", + }, + { + name: "Wrap Dress — Floral", + returnRate: 14.7, + issue: "Wrong size", + impact: "$8,200", + }, + { + name: "Oversized Hoodie — Black", + returnRate: 12.1, + issue: "Too large", + impact: "$6,900", + }, + { + name: "Ankle Boot — Tan", + returnRate: 11.8, + issue: "Wrong fit", + impact: "$5,400", + }, +]; + +const recommendations = [ + { + action: "Deploy dynamic size recommendation for top 3 SKUs", + impact: "Est. 22% reduction in fit returns", + priority: "High", + }, + { + action: "Add fit-specific review prompts to product pages", + impact: "Improve size confidence pre-purchase", + priority: "Medium", + }, + { + action: "Flag brands with >15% size variance for supplier review", + impact: "Address root cause across catalog", + priority: "Medium", + }, +]; + +const suggestedPrompts = [ + "Why are fit-related returns increasing?", + "Which products are driving return volume?", + "What orders are at risk of return?", +]; + +// --- Components --- + +function TrendChart({ data }: { data: typeof trendData }) { + const allValues = data.series.flatMap((s) => s.values); + const max = Math.max(...allValues); + const h = 60; + const w = 180; + const step = w / (data.weeks.length - 1); + + return ( +
+
+
+ Trend over time +
+

+ 8-week view of the top 3 return reasons +

+
+ + {data.series.map((series) => { + const points = series.values + .map((v, i) => `${i * step},${h - (v / max) * h * 0.85}`) + .join(" "); + return ( + + ); + })} + +
+ {data.series.map((s) => ( +
+
+ {s.label} +
+ ))} +
+
+

{data.takeaway}

+
+
+ ); +} + +function CategoryBreakdown() { + return ( +
+
+
+ Category breakdown +
+

+ Where "Wrong size/fit" returns are concentrated +

+
+
+ {categoryBreakdown.map((cat) => ( +
+
+ {cat.category} + {cat.pct}% +
+
+
+
+
+
+
+ ))} +
+
+

{categoryInsight}

+
+
+ ); +} + +function TopProducts() { + return ( +
+
+
+ Top products driving issues +
+

+ Ranked by return rate with revenue impact +

+
+
+
+ Product + Return % + Issue + Impact +
+ {topProducts.map((p) => ( +
+ {p.name} + + {p.returnRate}% + + + {p.issue} + + + {p.impact} + +
+ ))} +
+
+ ); +} + +function Recommendations() { + return ( +
+
+
+ Recommended actions +
+

+ AI-assisted next steps based on current data +

+
+
+ {recommendations.map((rec, i) => ( +
+
+ + {i + 1} + + + {rec.priority} + +
+

{rec.action}

+

+ {rec.impact} +

+
+ ))} +
+
+ ); +} + +// --- Exports --- + +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" }, +]; + +export function DrilldownTabContent({ tab }: { tab: DrilldownTab }) { + if (tab === "trend") return ; + if (tab === "categories") return ; + if (tab === "products") return ; + if (tab === "actions") return ; + return null; // "overview" is handled by the original card content +} + +export function AutopilotPrompts({ + onPromptSelect, +}: { + onPromptSelect?: (prompt: string) => void; +}) { + const [pressedPrompt, setPressedPrompt] = useState(null); + + return ( +
+
+ Autopilot + Autopilot + Ask Autopilot +
+
+ {suggestedPrompts.map((prompt) => ( + + ))} +
+
+ ); +} diff --git a/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx new file mode 100644 index 000000000..e57ca42a2 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/InsightGrid.tsx @@ -0,0 +1,508 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { ArrowUpRight, Maximize2, Minimize2 } from "lucide-react"; +import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + cardBgStyle, + getInsightCardClasses, + type CardConfig, + type InsightCardConfig, + type LayoutConfig, +} from "./glow-config"; +import { InsightCardBody } from "./insight-card-renderers"; +import { useDashboardData } from "./DashboardDataProvider"; +import { + type DrilldownTab, + drilldownTabs, + DrilldownTabContent, + AutopilotPrompts, +} from "./ExpandedInsightContent"; + +const sizeToFr: Record = { sm: "1fr", md: "2fr", lg: "1fr" }; + +// --- Shared card inner content --- + +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; + isAutopilotActive?: boolean; + className?: string; + style?: React.CSSProperties; +} + +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 + +
+ ) : ( +
+
+
+
+
+ )} +
+ )} + + ); +} + +// --- Main grid --- + +type ExpandPhase = "idle" | "width" | "height" | "full"; + +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: InsightCardConfig, 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, + ) + : undefined + } + 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, + ) + : undefined + } + isAutopilotActive={autopilotActiveIdx === idx} + style={{ + opacity: isSibling && phase !== "idle" ? 0 : 1, + transform: + isSibling && phase !== "idle" + ? "scale(0.95)" + : "scale(1)", + }} + /> + ); + })} +
+ ); + })} +
+ ); +} diff --git a/apps/apollo-vertex/templates/dashboard/PromptBar.tsx b/apps/apollo-vertex/templates/dashboard/PromptBar.tsx new file mode 100644 index 000000000..9bd863f88 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/PromptBar.tsx @@ -0,0 +1,193 @@ +"use client"; + +import { useState } from "react"; +import { MessagesSquare, Minimize2 } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import type { CardConfig, CardGradient } from "./glow-config"; +import { useDashboardData } from "./DashboardDataProvider"; + +function cardBgStyle( + bg: string, + opacity: number, + gradient: CardGradient, +): React.CSSProperties { + if (gradient.enabled) { + const alpha = gradient.opacity / 100; + return { + "--card-bg-override": `linear-gradient(${gradient.angle}deg, color-mix(in srgb, ${gradient.start} ${alpha * 100}%, transparent), color-mix(in srgb, ${gradient.end} ${alpha * 100}%, transparent))`, + borderColor: "transparent", + } as React.CSSProperties; + } + const value = + bg === "white" + ? `rgba(255,255,255,${opacity / 100})` + : `color-mix(in srgb, var(--${bg}) ${opacity}%, transparent)`; + return { "--card-bg-override": value } as React.CSSProperties; +} + +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/dev-controls-primitives.tsx b/apps/apollo-vertex/templates/dashboard/dev-controls-primitives.tsx new file mode 100644 index 000000000..a779dd432 --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/dev-controls-primitives.tsx @@ -0,0 +1,113 @@ +"use client"; + +export function Slider({ + label, + value, + min, + max, + step, + onChange, + displayValue, +}: { + label: string; + value: number; + min: number; + max: number; + step: number; + onChange: (v: number) => void; + displayValue?: string; +}) { + return ( +
+
+ {label} + {displayValue ?? value} +
+ onChange(Number(e.target.value))} + className="w-full h-1 accent-primary" + /> +
+ ); +} + +export function SelectControl({ + label, + value, + options, + onChange, +}: { + label: string; + value: string; + options: { label: string; value: string }[]; + onChange: (v: string) => void; +}) { + return ( +
+
{label}
+ +
+ ); +} + +export function Toggle({ + label, + checked, + onChange, +}: { + label: string; + checked: boolean; + onChange: (v: boolean) => void; +}) { + return ( + + ); +} + +export function TextInput({ + label, + value, + onChange, + placeholder, +}: { + label: string; + value: string; + onChange: (v: string) => void; + placeholder?: string; +}) { + return ( +
+
{label}
+ onChange(e.target.value)} + className="w-full h-7 rounded border bg-background px-1 text-xs" + placeholder={placeholder} + /> +
+ ); +} diff --git a/apps/apollo-vertex/templates/dashboard/dev-controls-tabs.tsx b/apps/apollo-vertex/templates/dashboard/dev-controls-tabs.tsx new file mode 100644 index 000000000..0ffc3c51d --- /dev/null +++ b/apps/apollo-vertex/templates/dashboard/dev-controls-tabs.tsx @@ -0,0 +1,396 @@ +"use client"; + +import { + SelectControl, + Slider, + TextInput, + Toggle, +} from "./dev-controls-primitives"; +import { + bgColorOptions, + cardTypeOptions, + chartTypeOptions, + containerBgOptions, + insightOptions, + interactionOptions, + isCardInteraction, + isCardSize, + isChartType, + isInsightCardType, + primaryOptions, + sizeOptions, + type CardConfig, + type CardGradient, + type GlowConfig, + type InsightCardConfig, + type LayoutConfig, +} from "./glow-config"; + +function GradientSection({ + gradient, + onChange, +}: { + gradient: CardGradient; + onChange: (g: CardGradient) => void; +}) { + const update = (partial: Partial) => + onChange({ ...gradient, ...partial }); + + return ( +
+ update({ enabled: v })} + /> + {gradient.enabled && ( + <> + update({ start: v })} + /> + update({ end: v })} + /> + update({ angle: v })} + displayValue={`${gradient.angle}°`} + /> + update({ opacity: v })} + displayValue={`${gradient.opacity}%`} + /> + + )} +
+ ); +} + +export function GlowTab({ + config, + onChange, +}: { + config: GlowConfig; + onChange: (c: GlowConfig) => void; +}) { + const update = (partial: Partial) => + onChange({ ...config, ...partial }); + + return ( +
+ update({ start: v })} + /> + update({ end: v })} + /> + update({ containerOpacity: v })} + displayValue={`${config.containerOpacity}%`} + /> + update({ fillOpacity: v })} + /> + update({ startStopOpacity: v })} + /> + update({ endStopOpacity: v })} + /> + update({ endOffset: v })} + /> +
+ ); +} + +export function CardsTab({ + config, + onChange, +}: { + config: CardConfig; + onChange: (c: CardConfig) => void; +}) { + const update = (partial: Partial) => + onChange({ ...config, ...partial }); + + return ( +
+
Overview Card
+ update({ overviewBg: v })} + /> + update({ overviewOpacity: v })} + displayValue={`${config.overviewOpacity}%`} + /> + update({ overviewGradient: g })} + /> + +
+ Insight Cards +
+ update({ insightBg: v })} + /> + update({ insightOpacity: v })} + displayValue={`${config.insightOpacity}%`} + /> + update({ insightGradient: g })} + /> + +
Prompt Bar
+ update({ promptBg: v })} + /> + update({ promptOpacity: v })} + displayValue={`${config.promptOpacity}%`} + /> + update({ promptGradient: g })} + /> + +
Shared
+ update({ borderVisible: v })} + /> + update({ backdropBlur: v })} + /> +
+ ); +} + +export function LayoutTab({ + config, + onChange, +}: { + config: LayoutConfig; + onChange: (c: LayoutConfig) => void; +}) { + const update = (partial: Partial) => + onChange({ ...config, ...partial }); + + const updateInsightCard = ( + index: number, + partial: Partial, + ) => { + const cards = [...config.insightCards] as [ + InsightCardConfig, + InsightCardConfig, + InsightCardConfig, + InsightCardConfig, + ]; + cards[index] = { ...cards[index], ...partial }; + update({ insightCards: cards }); + }; + + return ( +
+ update({ containerBg: v })} + /> + update({ gap: v })} + displayValue={`${config.gap}px`} + /> + update({ padding: v })} + displayValue={`${config.padding}px`} + /> +
Left Column
+ update({ overviewRatio: v })} + /> + update({ promptRatio: v })} + /> +
+ Insight Cards +
+ {["Top Left", "Top Right", "Bottom Left", "Bottom Right"].map( + (label, i) => ( +
+
+ {label} + updateInsightCard(i, { visible: v })} + /> +
+ {config.insightCards[i].visible && ( + <> + { + if (isCardSize(v)) updateInsightCard(i, { size: v }); + }} + /> + { + if (isInsightCardType(v)) + updateInsightCard(i, { + content: { ...config.insightCards[i].content, type: v }, + }); + }} + /> + {config.insightCards[i].content.type === "chart" && ( + { + if (isChartType(v)) + updateInsightCard(i, { + content: { + ...config.insightCards[i].content, + chartType: v, + }, + }); + }} + /> + )} + + updateInsightCard(i, { + content: { ...config.insightCards[i].content, title: v }, + }) + } + /> + { + if (isCardInteraction(v)) + updateInsightCard(i, { interaction: v }); + }} + /> + {config.insightCards[i].interaction === "navigate" && ( + updateInsightCard(i, { navigateTo: v })} + placeholder="/preview/dashboard/..." + /> + )} + + )} +
+ ), + )} +
+ ); +}