"use client"; import { useState, useRef, type ReactNode } from "react"; import { useBoundProp, defineRegistry } from "@json-render/react"; import { shadcnComponents } from "@json-render/shadcn"; import { Bar, BarChart as RechartsBarChart, CartesianGrid, Legend, Line, LineChart as RechartsLineChart, Pie, PieChart as RechartsPieChart, XAxis, } from "recharts"; import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig, } from "@/components/ui/chart"; import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell, } from "@/components/ui/table"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { TrendingUp, TrendingDown, Minus, Info, Lightbulb, AlertTriangle, Star, ArrowUpDown, ArrowUp, ArrowDown, } from "lucide-react"; // 3D imports import { Canvas, useFrame } from "@react-three/fiber"; import { OrbitControls, Stars as DreiStars, Text as DreiText, } from "@react-three/drei"; import type * as THREE from "three"; import { explorerCatalog } from "./catalog"; // ============================================================================= // 3D Helper Types & Components // ============================================================================= type Vec3Tuple = [number, number, number]; interface Animation3D { rotate?: number[] | null; } interface Mesh3DProps { position?: number[] | null; rotation?: number[] | null; scale?: number[] | null; color?: string | null; args?: number[] | null; metalness?: number | null; roughness?: number | null; emissive?: string | null; emissiveIntensity?: number | null; wireframe?: boolean | null; opacity?: number | null; animation?: Animation3D | null; } function toVec3(v: number[] | null | undefined): Vec3Tuple | undefined { if (!v || v.length < 3) return undefined; return v.slice(0, 3) as Vec3Tuple; } function toGeoArgs( v: number[] | null | undefined, fallback: T, ): T { if (!v || v.length === 0) return fallback; return v as unknown as T; } /** Shared hook for continuous rotation animation */ function useRotationAnimation( ref: React.RefObject, animation?: Animation3D | null, ) { useFrame(() => { if (!ref.current || !animation?.rotate) return; const [rx, ry, rz] = animation.rotate; ref.current.rotation.x += rx ?? 0; ref.current.rotation.y += ry ?? 0; ref.current.rotation.z += rz ?? 0; }); } /** Standard material props shared by all mesh primitives */ function StandardMaterial({ color, metalness, roughness, emissive, emissiveIntensity, wireframe, opacity, }: Mesh3DProps) { return ( ); } /** Generic mesh wrapper for all geometry primitives */ function MeshPrimitive({ meshProps, children, onClick, }: { meshProps: Mesh3DProps; children: ReactNode; onClick?: () => void; }) { const ref = useRef(null); useRotationAnimation(ref, meshProps.animation); return ( {children} ); } /** Animated group wrapper */ function AnimatedGroup({ position, rotation, scale, animation, children, }: { position?: number[] | null; rotation?: number[] | null; scale?: number[] | null; animation?: Animation3D | null; children?: ReactNode; }) { const ref = useRef(null); useRotationAnimation(ref, animation); return ( {children} ); } // ============================================================================= // Registry // ============================================================================= export const { registry, handlers } = defineRegistry(explorerCatalog, { components: { // From @json-render/shadcn (used as-is) Stack: shadcnComponents.Stack, Card: shadcnComponents.Card, Grid: shadcnComponents.Grid, Heading: shadcnComponents.Heading, Separator: shadcnComponents.Separator, Accordion: shadcnComponents.Accordion, Progress: shadcnComponents.Progress, Skeleton: shadcnComponents.Skeleton, Badge: shadcnComponents.Badge, Alert: shadcnComponents.Alert, // Chat-specific components Text: ({ props }) => (

{props.content}

), Metric: ({ props }) => { const TrendIcon = props.trend === "up" ? TrendingUp : props.trend === "down" ? TrendingDown : Minus; const trendColor = props.trend === "up" ? "text-green-500" : props.trend === "down" ? "text-red-500" : "text-muted-foreground"; return (

{props.label}

{props.value} {props.trend && }
{props.detail && (

{props.detail}

)}
); }, Table: ({ props }) => { const rawData = props.data; const items: Array> = Array.isArray(rawData) ? rawData : Array.isArray((rawData as Record)?.data) ? ((rawData as Record).data as Array< Record >) : []; const [sortKey, setSortKey] = useState(null); const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); if (items.length === 0) { return (
{props.emptyMessage ?? "No data"}
); } const sorted = sortKey ? [...items].sort((a, b) => { const av = a[sortKey]; const bv = b[sortKey]; // numeric comparison when both values are numbers if (typeof av === "number" && typeof bv === "number") { return sortDir === "asc" ? av - bv : bv - av; } const as = String(av ?? ""); const bs = String(bv ?? ""); return sortDir === "asc" ? as.localeCompare(bs) : bs.localeCompare(as); }) : items; const handleSort = (key: string) => { if (sortKey === key) { setSortDir((d) => (d === "asc" ? "desc" : "asc")); } else { setSortKey(key); setSortDir("asc"); } }; return ( {props.columns.map((col) => { const SortIcon = sortKey === col.key ? sortDir === "asc" ? ArrowUp : ArrowDown : ArrowUpDown; return ( ); })} {sorted.map((item, i) => ( {props.columns.map((col) => ( {String(item[col.key] ?? "")} ))} ))}
); }, Link: ({ props }) => ( {props.text} ), BarChart: ({ props }) => { const rawData = props.data; const rawItems: Array> = Array.isArray(rawData) ? rawData : Array.isArray((rawData as Record)?.data) ? ((rawData as Record).data as Array< Record >) : []; const { items, valueKey } = processChartData( rawItems, props.xKey, props.yKey, props.aggregate, ); const chartColor = props.color ?? "var(--chart-1)"; const chartConfig = { [valueKey]: { label: valueKey, color: chartColor, }, } satisfies ChartConfig; if (items.length === 0) { return (
No data available
); } return (
{props.title && (

{props.title}

)} } />
); }, LineChart: ({ props }) => { const rawData = props.data; const rawItems: Array> = Array.isArray(rawData) ? rawData : Array.isArray((rawData as Record)?.data) ? ((rawData as Record).data as Array< Record >) : []; const { items, valueKey } = processChartData( rawItems, props.xKey, props.yKey, props.aggregate, ); const chartColor = props.color ?? "var(--chart-1)"; const chartConfig = { [valueKey]: { label: valueKey, color: chartColor, }, } satisfies ChartConfig; if (items.length === 0) { return (
No data available
); } return (
{props.title && (

{props.title}

)} 12 ? Math.ceil(items.length / 8) - 1 : undefined } /> } />
); }, Tabs: ({ props, children }) => ( {(props.tabs ?? []).map((tab) => ( {tab.label} ))} {children} ), TabContent: ({ props, children }) => ( {children} ), Callout: ({ props }) => { const config = { info: { icon: Info, border: "border-l-blue-500", bg: "bg-blue-500/5", iconColor: "text-blue-500", }, tip: { icon: Lightbulb, border: "border-l-emerald-500", bg: "bg-emerald-500/5", iconColor: "text-emerald-500", }, warning: { icon: AlertTriangle, border: "border-l-amber-500", bg: "bg-amber-500/5", iconColor: "text-amber-500", }, important: { icon: Star, border: "border-l-purple-500", bg: "bg-purple-500/5", iconColor: "text-purple-500", }, }[props.type ?? "info"] ?? { icon: Info, border: "border-l-blue-500", bg: "bg-blue-500/5", iconColor: "text-blue-500", }; const Icon = config.icon; return (
{props.title && (

{props.title}

)}

{props.content}

); }, Timeline: ({ props }) => (
{/* Vertical line centered on dots: dot is 12px wide starting at 0px, center = 6px */}
{(props.items ?? []).map((item, i) => { const dotColor = item.status === "completed" ? "bg-emerald-500" : item.status === "current" ? "bg-blue-500" : "bg-muted-foreground/30"; return (

{item.title}

{item.date && ( {item.date} )}
{item.description && (

{item.description}

)}
); })}
), PieChart: ({ props }) => { const rawData = props.data; const items: Array> = Array.isArray(rawData) ? rawData : Array.isArray((rawData as Record)?.data) ? ((rawData as Record).data as Array< Record >) : []; if (items.length === 0) { return (
No data available
); } const chartConfig: ChartConfig = {}; items.forEach((item, i) => { const name = String(item[props.nameKey] ?? `Segment ${i + 1}`); chartConfig[name] = { label: name, color: PIE_COLORS[i % PIE_COLORS.length], }; }); return (
{props.title && (

{props.title}

)} } /> ({ name: String(item[props.nameKey] ?? `Segment ${i + 1}`), value: typeof item[props.valueKey] === "number" ? item[props.valueKey] : parseFloat(String(item[props.valueKey])) || 0, fill: PIE_COLORS[i % PIE_COLORS.length], }))} dataKey="value" nameKey="name" innerRadius="40%" outerRadius="70%" paddingAngle={2} />
); }, RadioGroup: ({ props, bindings }) => { const [value, setValue] = useBoundProp( props.value as string | undefined, bindings?.value, ); const current = value ?? ""; return (
{props.label && ( )} setValue(v)} > {(props.options ?? []).map((opt) => (
))}
); }, SelectInput: ({ props, bindings }) => { const [value, setValue] = useBoundProp( props.value as string | undefined, bindings?.value, ); const current = value ?? ""; return (
{props.label && ( )}
); }, TextInput: ({ props, bindings }) => { const [value, setValue] = useBoundProp( props.value as string | undefined, bindings?.value, ); const current = value ?? ""; return (
{props.label && ( )} setValue(e.target.value)} />
); }, Button: ({ props, emit }) => ( ), // ========================================================================= // 3D Scene Components // ========================================================================= Scene3D: ({ props, children }) => (
{children}
), Group3D: ({ props, children }) => ( {children} ), Box: ({ props, emit }) => ( emit("press")}> (props.args, [1, 1, 1])} /> ), Sphere: ({ props, emit }) => ( emit("press")}> (props.args, [1, 32, 32])} /> ), Cylinder: ({ props, emit }) => ( emit("press")}> ( props.args, [1, 1, 2, 32], )} /> ), Cone: ({ props, emit }) => ( emit("press")}> (props.args, [1, 2, 32])} /> ), Torus: ({ props, emit }) => ( emit("press")}> ( props.args, [1, 0.4, 16, 100], )} /> ), Plane: ({ props, emit }) => ( emit("press")}> (props.args, [10, 10])} /> ), Ring: ({ props, emit }) => ( emit("press")}> (props.args, [0.5, 1, 64])} /> ), AmbientLight: ({ props }) => ( ), PointLight: ({ props }) => ( ), DirectionalLight: ({ props }) => ( ), Stars: ({ props }) => ( ), Label3D: ({ props }) => ( {props.text} ), }, }); // ============================================================================= // Chart Helpers // ============================================================================= const PIE_COLORS = [ "var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)", "var(--chart-5)", ]; function processChartData( items: Array>, xKey: string, yKey: string, aggregate: "sum" | "count" | "avg" | null | undefined, ): { items: Array>; valueKey: string } { if (items.length === 0) { return { items: [], valueKey: yKey }; } if (!aggregate) { const formatted = items.map((item) => ({ ...item, label: String(item[xKey] ?? ""), })); return { items: formatted, valueKey: yKey }; } const groups = new Map>>(); for (const item of items) { const groupKey = String(item[xKey] ?? "unknown"); const group = groups.get(groupKey) ?? []; group.push(item); groups.set(groupKey, group); } const valueKey = aggregate === "count" ? "count" : yKey; const aggregated: Array> = []; const sortedKeys = Array.from(groups.keys()).sort(); for (const key of sortedKeys) { const group = groups.get(key)!; let value: number; if (aggregate === "count") { value = group.length; } else if (aggregate === "sum") { value = group.reduce((sum, item) => { const v = item[yKey]; return sum + (typeof v === "number" ? v : parseFloat(String(v)) || 0); }, 0); } else { const sum = group.reduce((s, item) => { const v = item[yKey]; return s + (typeof v === "number" ? v : parseFloat(String(v)) || 0); }, 0); value = group.length > 0 ? sum / group.length : 0; } aggregated.push({ label: key, [valueKey]: value }); } return { items: aggregated, valueKey }; } // ============================================================================= // Fallback Component // ============================================================================= export function Fallback({ type }: { type: string }) { return (
Unknown component: {type}
); }