/** * Dynamic UI Renderer * * Renders agent-generated UI specs using shadcn/ui components. * Handles action callbacks for interactive elements. */ "use client" import { useCallback } from "react" import { executeAction } from "@/lib/agent/chat-adapter" import type { ComponentSpec } from "@/lib/agent/catalog" // Import shadcn components import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { Progress } from "@/components/ui/progress" import { cn } from "@/lib/utils" interface DynamicUIProps { spec: ComponentSpec className?: string } export function DynamicUI({ spec, className }: DynamicUIProps) { const handleAction = useCallback( async (action: { type: string; payload?: Record }) => { await executeAction(action) }, [] ) return (
) } interface RendererProps { spec: ComponentSpec onAction: (action: { type: string; payload?: Record }) => void } function ComponentRenderer({ spec, onAction }: RendererProps) { switch (spec.type) { case "DataTable": return case "Card": return case "Badge": return {spec.props.label} case "StatCard": return case "Button": return ( ) case "ButtonGroup": return (
{spec.props.buttons.map((btn, i) => ( ))}
) case "InvoiceTable": return case "CustomerCard": return case "VendorCard": return case "SchedulePreview": return case "ProjectSummary": return case "Grid": return (
{(spec.props.children as ComponentSpec[])?.map((child, i) => ( ))}
) case "Stack": return (
{(spec.props.children as ComponentSpec[])?.map((child, i) => ( ))}
) default: return (
Unknown component type: {(spec as { type: string }).type}
) } } // DataTable renderer function DataTableRenderer({ columns, data, onRowClick, onAction, }: { columns: Array<{ key: string; header: string; format?: string }> data: Array> onRowClick?: { type: string; payload?: Record } onAction: RendererProps["onAction"] }) { return (
{columns.map((col) => ( {col.header} ))} {data.map((row, i) => ( onRowClick && onAction({ ...onRowClick, payload: { ...onRowClick.payload, rowData: row }, }) } > {columns.map((col) => ( {formatValue(row[col.key], col.format)} ))} ))}
) } // Card renderer function CardRenderer({ title, description, children, footer, onAction, }: { title: string description?: string children?: unknown[] footer?: string onAction: RendererProps["onAction"] }) { return ( {title} {description && {description}} {children && children.length > 0 && ( {(children as ComponentSpec[]).map((child, i) => ( ))} )} {footer && (

{footer}

)}
) } // StatCard renderer function StatCardRenderer({ title, value, change, changeLabel, }: { title: string value: string | number change?: number changeLabel?: string }) { return ( {title} {value} {change !== undefined && (
= 0 ? "text-green-600" : "text-red-600"}> {change >= 0 ? "+" : ""} {change}% {changeLabel && ` ${changeLabel}`}
)}
) } // Invoice table renderer function InvoiceTableRenderer({ invoices, onRowClick, onAction, }: { invoices: Array<{ id: string number: string customer: string amount: number dueDate: string status: string }> onRowClick?: { type: string; payload?: Record } onAction: RendererProps["onAction"] }) { const statusVariant = (status: string) => { switch (status) { case "paid": return "default" case "overdue": return "destructive" default: return "secondary" } } return (
Invoice # Customer Amount Due Date Status {invoices.map((invoice) => ( onRowClick && onAction({ ...onRowClick, payload: { ...onRowClick.payload, invoiceId: invoice.id }, }) } > {invoice.number} {invoice.customer} {formatCurrency(invoice.amount)} {formatDate(invoice.dueDate)} {invoice.status} ))}
) } // Customer card renderer function CustomerCardRenderer({ customer, actions, onAction, }: { customer: { id: string name: string company?: string email?: string phone?: string } actions?: Array<{ type: string; payload?: Record }> onAction: RendererProps["onAction"] }) { return ( {customer.name} {customer.company && ( {customer.company} )} {customer.email &&

Email: {customer.email}

} {customer.phone &&

Phone: {customer.phone}

}
{actions && actions.length > 0 && ( {actions.map((action, i) => ( ))} )}
) } // Vendor card renderer function VendorCardRenderer({ vendor, actions, onAction, }: { vendor: { id: string name: string category: string email?: string phone?: string } actions?: Array<{ type: string; payload?: Record }> onAction: RendererProps["onAction"] }) { return (
{vendor.name} {vendor.category}
{vendor.email &&

Email: {vendor.email}

} {vendor.phone &&

Phone: {vendor.phone}

}
{actions && actions.length > 0 && ( {actions.map((action, i) => ( ))} )}
) } // Schedule preview renderer function SchedulePreviewRenderer({ projectName, tasks, onTaskClick, onAction, }: { projectId: string projectName: string tasks: Array<{ id: string title: string startDate: string endDate: string phase: string status: string percentComplete: number isCriticalPath?: boolean }> onTaskClick?: { type: string; payload?: Record } onAction: RendererProps["onAction"] }) { return ( {projectName} Schedule {tasks.length} tasks {tasks.slice(0, 5).map((task) => (
onTaskClick && onAction({ ...onTaskClick, payload: { ...onTaskClick.payload, taskId: task.id }, }) } >
{task.title} {task.phase}
{task.percentComplete}% complete {formatDate(task.startDate)} - {formatDate(task.endDate)}
))} {tasks.length > 5 && (

+{tasks.length - 5} more tasks

)}
) } // Project summary renderer function ProjectSummaryRenderer({ project, stats, actions, onAction, }: { project: { id: string name: string status: string address?: string clientName?: string projectManager?: string } stats?: { tasksTotal: number tasksComplete: number daysRemaining?: number budgetUsed?: number } actions?: Array<{ type: string; payload?: Record }> onAction: RendererProps["onAction"] }) { const completion = stats ? Math.round((stats.tasksComplete / stats.tasksTotal) * 100) : 0 return (
{project.name} {project.status}
{project.address && ( {project.address} )}
{project.clientName && (

Client: {project.clientName}

)} {project.projectManager && (

PM: {project.projectManager}

)} {stats && (
Progress {stats.tasksComplete}/{stats.tasksTotal} tasks ({completion}%)
)}
{actions && actions.length > 0 && ( {actions.map((action, i) => ( ))} )}
) } // Utility functions function formatValue(value: unknown, format?: string): React.ReactNode { if (value === null || value === undefined) return "-" switch (format) { case "currency": return formatCurrency(Number(value)) case "date": return formatDate(String(value)) case "badge": return {String(value)} default: return String(value) } } function formatCurrency(amount: number): string { return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }).format(amount) } function formatDate(dateStr: string): string { try { return new Date(dateStr).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", }) } catch { return dateStr } } export default DynamicUI