Chris Tate 5 ay önce
ebeveyn
işleme
64cfd22d96

+ 0 - 80
apps/dashboard/app/api/generate/route.ts

@@ -1,80 +0,0 @@
-import { streamText } from 'ai';
-import { gateway } from '@ai-sdk/gateway';
-import { componentList } from '@/lib/catalog';
-
-export const maxDuration = 30;
-
-const SYSTEM_PROMPT = `You are a dashboard widget generator that outputs JSONL (JSON Lines) patches.
-
-AVAILABLE COMPONENTS:
-${componentList.join(', ')}
-
-COMPONENT DETAILS:
-- Card: { title?: string, description?: string, padding?: "sm"|"md"|"lg" } - Container with optional title
-- Grid: { columns?: 1-4, gap?: "sm"|"md"|"lg" } - Grid layout
-- Stack: { direction?: "horizontal"|"vertical", gap?: "sm"|"md"|"lg", align?: "start"|"center"|"end"|"stretch" } - Flex layout
-- Metric: { label: string, valuePath: string, format?: "number"|"currency"|"percent", trend?: "up"|"down"|"neutral", trendValue?: string }
-- Chart: { type: "bar"|"line"|"pie"|"area", dataPath: string, title?: string, height?: number }
-- Table: { dataPath: string, columns: [{ key: string, label: string, format?: "text"|"currency"|"date"|"badge" }] }
-- Button: { label: string, action: string, variant?: "primary"|"secondary"|"danger"|"ghost" }
-- Heading: { text: string, level?: "h1"|"h2"|"h3"|"h4" }
-- Text: { content: string, variant?: "body"|"caption"|"label", color?: "default"|"muted"|"success"|"warning"|"danger" }
-- Badge: { text: string, variant?: "default"|"success"|"warning"|"danger"|"info" }
-- Alert: { type: "info"|"success"|"warning"|"error", title: string, message?: string }
-
-DATA BINDING:
-- valuePath: "/analytics/revenue" (for single values like Metric)
-- dataPath: "/analytics/salesByRegion" (for arrays like Chart, Table)
-
-OUTPUT FORMAT:
-Output JSONL where each line is a patch operation. Use a FLAT key-based structure:
-
-OPERATIONS:
-- {"op":"set","path":"/root","value":"main-card"} - Set the root element key
-- {"op":"add","path":"/elements/main-card","value":{...}} - Add an element by unique key
-
-ELEMENT STRUCTURE:
-{
-  "key": "unique-key",
-  "type": "ComponentType",
-  "props": { ... },
-  "children": ["child-key-1", "child-key-2"]  // Array of child element keys
-}
-
-RULES:
-1. First set /root to the root element's key
-2. Add each element with a unique key using /elements/{key}
-3. Parent elements list child keys in their "children" array
-4. Stream elements progressively - parent first, then children
-5. Each element must have: key, type, props
-6. Children array contains STRING KEYS, not nested objects
-
-EXAMPLE - Revenue Dashboard:
-{"op":"set","path":"/root","value":"main-card"}
-{"op":"add","path":"/elements/main-card","value":{"key":"main-card","type":"Card","props":{"title":"Revenue Dashboard","padding":"md"},"children":["metrics-grid","chart"]}}
-{"op":"add","path":"/elements/metrics-grid","value":{"key":"metrics-grid","type":"Grid","props":{"columns":2,"gap":"md"},"children":["revenue-metric","growth-metric"]}}
-{"op":"add","path":"/elements/revenue-metric","value":{"key":"revenue-metric","type":"Metric","props":{"label":"Total Revenue","valuePath":"/analytics/revenue","format":"currency","trend":"up","trendValue":"+15%"}}}
-{"op":"add","path":"/elements/growth-metric","value":{"key":"growth-metric","type":"Metric","props":{"label":"Growth Rate","valuePath":"/analytics/growth","format":"percent"}}}
-{"op":"add","path":"/elements/chart","value":{"key":"chart","type":"Chart","props":{"type":"bar","dataPath":"/analytics/salesByRegion","title":"Sales by Region"}}}
-
-Generate JSONL patches now:`;
-
-export async function POST(req: Request) {
-  const { prompt, context } = await req.json();
-
-  let fullPrompt = prompt;
-
-  // Add data context
-  if (context?.data) {
-    fullPrompt += `\n\nAVAILABLE DATA:\n${JSON.stringify(context.data, null, 2)}`;
-  }
-
-  const result = streamText({
-    model: gateway('openai/gpt-4o'),
-    system: SYSTEM_PROMPT,
-    prompt: fullPrompt,
-    temperature: 0.7,
-  });
-
-  return result.toTextStreamResponse();
-}

+ 0 - 28
apps/dashboard/app/layout.tsx

@@ -1,28 +0,0 @@
-import type { Metadata } from 'next';
-
-export const metadata: Metadata = {
-  title: 'json-render Dashboard Demo',
-  description: 'AI-powered dashboard widget generator with guardrails',
-};
-
-export default function RootLayout({
-  children,
-}: {
-  children: React.ReactNode;
-}) {
-  return (
-    <html lang="en">
-      <body
-        style={{
-          margin: 0,
-          fontFamily:
-            '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
-          backgroundColor: '#f9fafb',
-          minHeight: '100vh',
-        }}
-      >
-        {children}
-      </body>
-    </html>
-  );
-}

+ 0 - 317
apps/dashboard/app/page.tsx

@@ -1,317 +0,0 @@
-'use client';
-
-import { useState, useCallback } from 'react';
-import {
-  DataProvider,
-  ActionProvider,
-  VisibilityProvider,
-  useUIStream,
-  Renderer,
-} from '@json-render/react';
-import { componentRegistry } from '@/components/ui-components';
-
-// Sample data for the dashboard
-const INITIAL_DATA = {
-  analytics: {
-    revenue: 125000,
-    growth: 0.15,
-    customers: 1234,
-    orders: 567,
-    salesByRegion: [
-      { label: 'US', value: 45000 },
-      { label: 'EU', value: 35000 },
-      { label: 'Asia', value: 28000 },
-      { label: 'Other', value: 17000 },
-    ],
-    recentTransactions: [
-      { id: 'TXN001', customer: 'Acme Corp', amount: 1500, status: 'completed', date: '2024-01-15' },
-      { id: 'TXN002', customer: 'Globex Inc', amount: 2300, status: 'pending', date: '2024-01-14' },
-      { id: 'TXN003', customer: 'Initech', amount: 890, status: 'completed', date: '2024-01-13' },
-      { id: 'TXN004', customer: 'Umbrella Co', amount: 4200, status: 'completed', date: '2024-01-12' },
-    ],
-  },
-  form: {
-    dateRange: '',
-    region: '',
-  },
-};
-
-// Action handlers
-const ACTION_HANDLERS = {
-  export_report: () => {
-    alert('Exporting report... (demo)');
-  },
-  refresh_data: () => {
-    alert('Refreshing data... (demo)');
-  },
-  view_details: (params: Record<string, unknown>) => {
-    alert(`Viewing details for: ${JSON.stringify(params)}`);
-  },
-  apply_filter: () => {
-    alert('Applying filters... (demo)');
-  },
-};
-
-function DashboardContent() {
-  const [prompt, setPrompt] = useState('');
-
-  const {
-    tree,
-    isStreaming,
-    error,
-    send,
-    clear,
-  } = useUIStream({
-    api: '/api/generate',
-    onError: (err) => console.error('Generation error:', err),
-  });
-
-  const handleSubmit = useCallback(
-    async (e: React.FormEvent) => {
-      e.preventDefault();
-      if (!prompt.trim()) return;
-      await send(prompt, { data: INITIAL_DATA });
-    },
-    [prompt, send]
-  );
-
-  const examplePrompts = [
-    'Show me a revenue dashboard with key metrics and a chart of sales by region',
-    'Create a transactions table with recent orders',
-    'Build a widget showing customer count with a trend indicator',
-    'Make a simple card with total revenue and an export button',
-  ];
-
-  const hasElements = tree && Object.keys(tree.elements).length > 0;
-
-  return (
-    <div style={{ maxWidth: '1200px', margin: '0 auto', padding: '24px' }}>
-      {/* Header */}
-      <div style={{ marginBottom: '32px' }}>
-        <h1
-          style={{
-            margin: '0 0 8px 0',
-            fontSize: '28px',
-            fontWeight: 700,
-            color: '#111827',
-          }}
-        >
-          json-render Dashboard Demo
-        </h1>
-        <p style={{ margin: 0, color: '#6b7280', fontSize: '16px' }}>
-          AI-powered widget generation with guardrails. Only catalog components can be generated.
-        </p>
-      </div>
-
-      {/* Input Form */}
-      <div
-        style={{
-          backgroundColor: '#fff',
-          borderRadius: '12px',
-          border: '1px solid #e5e7eb',
-          padding: '20px',
-          marginBottom: '24px',
-        }}
-      >
-        <form onSubmit={handleSubmit}>
-          <div style={{ display: 'flex', gap: '12px', marginBottom: '16px' }}>
-            <input
-              type="text"
-              value={prompt}
-              onChange={(e) => setPrompt(e.target.value)}
-              placeholder="Describe the widget you want to create..."
-              style={{
-                flex: 1,
-                padding: '12px 16px',
-                borderRadius: '8px',
-                border: '1px solid #d1d5db',
-                fontSize: '15px',
-                outline: 'none',
-              }}
-              disabled={isStreaming}
-            />
-            <button
-              type="submit"
-              disabled={isStreaming || !prompt.trim()}
-              style={{
-                padding: '12px 24px',
-                backgroundColor: isStreaming ? '#9ca3af' : '#6366f1',
-                color: '#fff',
-                border: 'none',
-                borderRadius: '8px',
-                fontSize: '15px',
-                fontWeight: 500,
-                cursor: isStreaming ? 'not-allowed' : 'pointer',
-              }}
-            >
-              {isStreaming ? 'Generating...' : 'Generate'}
-            </button>
-            <button
-              type="button"
-              onClick={clear}
-              style={{
-                padding: '12px 20px',
-                backgroundColor: '#fff',
-                color: '#374151',
-                border: '1px solid #d1d5db',
-                borderRadius: '8px',
-                fontSize: '15px',
-                fontWeight: 500,
-                cursor: 'pointer',
-              }}
-            >
-              Clear
-            </button>
-          </div>
-
-          {/* Example Prompts */}
-          <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
-            <span style={{ fontSize: '13px', color: '#6b7280', marginRight: '4px' }}>
-              Try:
-            </span>
-            {examplePrompts.map((example, i) => (
-              <button
-                key={i}
-                type="button"
-                onClick={() => setPrompt(example)}
-                style={{
-                  padding: '4px 10px',
-                  backgroundColor: '#f3f4f6',
-                  border: 'none',
-                  borderRadius: '4px',
-                  fontSize: '12px',
-                  color: '#4b5563',
-                  cursor: 'pointer',
-                }}
-              >
-                {example.slice(0, 40)}...
-              </button>
-            ))}
-          </div>
-        </form>
-      </div>
-
-      {/* Error Display */}
-      {error && (
-        <div
-          style={{
-            padding: '16px',
-            backgroundColor: '#fef2f2',
-            border: '1px solid #fca5a5',
-            borderRadius: '8px',
-            marginBottom: '24px',
-            color: '#991b1b',
-          }}
-        >
-          <strong>Error:</strong> {error.message}
-        </div>
-      )}
-
-      {/* Generated UI */}
-      <div
-        style={{
-          backgroundColor: '#fff',
-          borderRadius: '12px',
-          border: '1px solid #e5e7eb',
-          padding: '24px',
-          minHeight: '300px',
-        }}
-      >
-        {!hasElements && !isStreaming ? (
-          <div style={{ textAlign: 'center', padding: '60px 20px', color: '#9ca3af' }}>
-            <div style={{ fontSize: '48px', marginBottom: '16px' }}>🎨</div>
-            <p style={{ fontSize: '16px', margin: 0 }}>
-              Enter a prompt above to generate a dashboard widget
-            </p>
-          </div>
-        ) : tree ? (
-          <Renderer
-            tree={tree}
-            registry={componentRegistry}
-            loading={isStreaming}
-          />
-        ) : null}
-      </div>
-
-      {/* Debug: Show generated JSON */}
-      {hasElements && (
-        <details style={{ marginTop: '24px' }}>
-          <summary
-            style={{
-              cursor: 'pointer',
-              fontSize: '14px',
-              color: '#6b7280',
-              marginBottom: '8px',
-            }}
-          >
-            View Generated JSON
-          </summary>
-          <pre
-            style={{
-              backgroundColor: '#1f2937',
-              color: '#e5e7eb',
-              padding: '16px',
-              borderRadius: '8px',
-              overflow: 'auto',
-              fontSize: '12px',
-            }}
-          >
-            {JSON.stringify(tree, null, 2)}
-          </pre>
-        </details>
-      )}
-
-      {/* Info Box */}
-      <div
-        style={{
-          marginTop: '32px',
-          padding: '20px',
-          backgroundColor: '#eff6ff',
-          borderRadius: '8px',
-          border: '1px solid #bfdbfe',
-        }}
-      >
-        <h3 style={{ margin: '0 0 12px 0', fontSize: '16px', color: '#1e40af' }}>
-          How json-render Works
-        </h3>
-        <ul
-          style={{
-            margin: 0,
-            paddingLeft: '20px',
-            color: '#1e40af',
-            fontSize: '14px',
-            lineHeight: 1.6,
-          }}
-        >
-          <li>
-            <strong>Guardrails:</strong> AI can only generate components from the catalog (Card,
-            Metric, Chart, etc.)
-          </li>
-          <li>
-            <strong>Data Binding:</strong> Components reference data paths like{' '}
-            <code>/analytics/revenue</code>
-          </li>
-          <li>
-            <strong>Named Actions:</strong> Buttons declare actions like{' '}
-            <code>export_report</code> - you control what they do
-          </li>
-          <li>
-            <strong>Your Design System:</strong> The catalog maps to your actual React components
-          </li>
-        </ul>
-      </div>
-    </div>
-  );
-}
-
-export default function DashboardPage() {
-  return (
-    <DataProvider initialData={INITIAL_DATA}>
-      <VisibilityProvider>
-        <ActionProvider handlers={ACTION_HANDLERS}>
-          <DashboardContent />
-        </ActionProvider>
-      </VisibilityProvider>
-    </DataProvider>
-  );
-}

+ 0 - 716
apps/dashboard/components/ui-components.tsx

@@ -1,716 +0,0 @@
-'use client';
-
-import React, { type ReactNode } from 'react';
-import { type ComponentRenderProps } from '@json-render/react';
-import { useData, useFieldValidation } from '@json-render/react';
-import { getByPath } from '@json-render/core';
-
-// Helper to get value from data model
-function useResolvedValue<T>(value: T | { path: string } | null | undefined): T | undefined {
-  const { data } = useData();
-  
-  if (value === null || value === undefined) {
-    return undefined;
-  }
-  
-  if (typeof value === 'object' && 'path' in value) {
-    return getByPath(data, value.path) as T | undefined;
-  }
-  
-  return value as T;
-}
-
-// Styles
-const styles = {
-  card: {
-    backgroundColor: '#fff',
-    borderRadius: '8px',
-    border: '1px solid #e5e7eb',
-    overflow: 'hidden',
-  },
-  cardHeader: {
-    padding: '16px 20px',
-    borderBottom: '1px solid #e5e7eb',
-  },
-  cardBody: (padding: string | null | undefined) => ({
-    padding:
-      padding === 'none' ? 0 :
-      padding === 'sm' ? '12px' :
-      padding === 'lg' ? '24px' : '16px',
-  }),
-  metric: {
-    display: 'flex',
-    flexDirection: 'column' as const,
-    gap: '4px',
-  },
-  metricLabel: {
-    fontSize: '14px',
-    color: '#6b7280',
-  },
-  metricValue: {
-    fontSize: '32px',
-    fontWeight: 600,
-    color: '#111827',
-  },
-  metricTrend: (trend: string | null | undefined) => ({
-    fontSize: '14px',
-    color: trend === 'up' ? '#10b981' : trend === 'down' ? '#ef4444' : '#6b7280',
-  }),
-};
-
-// Card component
-export function Card({ element, children }: ComponentRenderProps) {
-  const { title, description, padding } = element.props as {
-    title?: string | null;
-    description?: string | null;
-    padding?: string | null;
-  };
-
-  return (
-    <div style={styles.card}>
-      {(title || description) && (
-        <div style={styles.cardHeader}>
-          {title && (
-            <h3 style={{ margin: 0, fontSize: '16px', fontWeight: 600 }}>{title}</h3>
-          )}
-          {description && (
-            <p style={{ margin: '4px 0 0', fontSize: '14px', color: '#6b7280' }}>
-              {description}
-            </p>
-          )}
-        </div>
-      )}
-      <div style={styles.cardBody(padding)}>{children}</div>
-    </div>
-  );
-}
-
-// Heading component
-export function Heading({ element }: ComponentRenderProps) {
-  const { text, level } = element.props as {
-    text: string;
-    level?: string | null;
-  };
-
-  const Tag = (level || 'h2') as keyof JSX.IntrinsicElements;
-  const sizes: Record<string, string> = {
-    h1: '28px',
-    h2: '24px',
-    h3: '20px',
-    h4: '16px',
-  };
-
-  return (
-    <Tag style={{ margin: '0 0 16px', fontSize: sizes[level || 'h2'], fontWeight: 600 }}>
-      {text}
-    </Tag>
-  );
-}
-
-// Text component
-export function Text({ element }: ComponentRenderProps) {
-  const { content, variant } = element.props as {
-    content: string;
-    variant?: string | null;
-  };
-
-  const colors: Record<string, string> = {
-    default: '#111827',
-    muted: '#6b7280',
-    success: '#10b981',
-    warning: '#f59e0b',
-    error: '#ef4444',
-  };
-
-  return (
-    <p style={{ margin: 0, color: colors[variant || 'default'] }}>{content}</p>
-  );
-}
-
-// Metric component
-export function Metric({ element }: ComponentRenderProps) {
-  const { label, valuePath, format, trend, trendValue } = element.props as {
-    label: string;
-    valuePath: string;
-    format?: string | null;
-    trend?: string | null;
-    trendValue?: string | null;
-  };
-
-  const { data } = useData();
-  const rawValue = getByPath(data, valuePath);
-
-  let displayValue = String(rawValue ?? '—');
-  if (format === 'currency' && typeof rawValue === 'number') {
-    displayValue = new Intl.NumberFormat('en-US', {
-      style: 'currency',
-      currency: 'USD',
-    }).format(rawValue);
-  } else if (format === 'percent' && typeof rawValue === 'number') {
-    displayValue = new Intl.NumberFormat('en-US', {
-      style: 'percent',
-      minimumFractionDigits: 1,
-    }).format(rawValue);
-  } else if (format === 'number' && typeof rawValue === 'number') {
-    displayValue = new Intl.NumberFormat('en-US').format(rawValue);
-  }
-
-  return (
-    <div style={styles.metric}>
-      <span style={styles.metricLabel}>{label}</span>
-      <span style={styles.metricValue}>{displayValue}</span>
-      {(trend || trendValue) && (
-        <span style={styles.metricTrend(trend)}>
-          {trend === 'up' ? '↑' : trend === 'down' ? '↓' : ''} {trendValue}
-        </span>
-      )}
-    </div>
-  );
-}
-
-// Chart component (simplified placeholder)
-export function Chart({ element }: ComponentRenderProps) {
-  const { title, dataPath, type } = element.props as {
-    title?: string | null;
-    dataPath: string;
-    type?: string | null;
-  };
-
-  const { data } = useData();
-  const chartData = getByPath(data, dataPath) as Array<{ label: string; value: number }> | undefined;
-
-  if (!chartData || !Array.isArray(chartData)) {
-    return <div style={{ padding: '20px', color: '#6b7280' }}>No chart data</div>;
-  }
-
-  const maxValue = Math.max(...chartData.map((d) => d.value));
-
-  return (
-    <div>
-      {title && (
-        <h4 style={{ margin: '0 0 16px', fontSize: '14px', fontWeight: 600 }}>{title}</h4>
-      )}
-      <div style={{ display: 'flex', gap: '8px', alignItems: 'flex-end', height: '120px' }}>
-        {chartData.map((d, i) => (
-          <div
-            key={i}
-            style={{
-              flex: 1,
-              display: 'flex',
-              flexDirection: 'column',
-              alignItems: 'center',
-              gap: '4px',
-            }}
-          >
-            <div
-              style={{
-                width: '100%',
-                height: `${(d.value / maxValue) * 100}%`,
-                backgroundColor: '#6366f1',
-                borderRadius: '4px 4px 0 0',
-                minHeight: '4px',
-              }}
-            />
-            <span style={{ fontSize: '12px', color: '#6b7280' }}>{d.label}</span>
-          </div>
-        ))}
-      </div>
-    </div>
-  );
-}
-
-// Table component
-export function Table({ element }: ComponentRenderProps) {
-  const { title, dataPath, columns } = element.props as {
-    title?: string | null;
-    dataPath: string;
-    columns: Array<{ key: string; label: string; format?: string | null }>;
-  };
-
-  const { data } = useData();
-  const tableData = getByPath(data, dataPath) as Array<Record<string, unknown>> | undefined;
-
-  if (!tableData || !Array.isArray(tableData)) {
-    return <div style={{ padding: '20px', color: '#6b7280' }}>No data</div>;
-  }
-
-  const formatCell = (value: unknown, format?: string | null) => {
-    if (value === null || value === undefined) return '—';
-    if (format === 'currency' && typeof value === 'number') {
-      return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value);
-    }
-    if (format === 'date' && typeof value === 'string') {
-      return new Date(value).toLocaleDateString();
-    }
-    if (format === 'badge') {
-      const colors: Record<string, { bg: string; text: string }> = {
-        completed: { bg: '#d1fae5', text: '#065f46' },
-        pending: { bg: '#fef3c7', text: '#92400e' },
-        failed: { bg: '#fee2e2', text: '#991b1b' },
-      };
-      const style = colors[String(value).toLowerCase()] || { bg: '#f3f4f6', text: '#374151' };
-      return (
-        <span
-          style={{
-            padding: '2px 8px',
-            borderRadius: '12px',
-            fontSize: '12px',
-            fontWeight: 500,
-            backgroundColor: style.bg,
-            color: style.text,
-          }}
-        >
-          {String(value)}
-        </span>
-      );
-    }
-    return String(value);
-  };
-
-  return (
-    <div>
-      {title && (
-        <h4 style={{ margin: '0 0 16px', fontSize: '14px', fontWeight: 600 }}>{title}</h4>
-      )}
-      <table style={{ width: '100%', borderCollapse: 'collapse' }}>
-        <thead>
-          <tr>
-            {columns.map((col) => (
-              <th
-                key={col.key}
-                style={{
-                  textAlign: 'left',
-                  padding: '12px 8px',
-                  borderBottom: '1px solid #e5e7eb',
-                  fontSize: '12px',
-                  fontWeight: 600,
-                  color: '#6b7280',
-                  textTransform: 'uppercase',
-                }}
-              >
-                {col.label}
-              </th>
-            ))}
-          </tr>
-        </thead>
-        <tbody>
-          {tableData.map((row, i) => (
-            <tr key={i}>
-              {columns.map((col) => (
-                <td
-                  key={col.key}
-                  style={{
-                    padding: '12px 8px',
-                    borderBottom: '1px solid #e5e7eb',
-                    fontSize: '14px',
-                  }}
-                >
-                  {formatCell(row[col.key], col.format)}
-                </td>
-              ))}
-            </tr>
-          ))}
-        </tbody>
-      </table>
-    </div>
-  );
-}
-
-// Button component
-export function Button({ element, onAction, loading }: ComponentRenderProps) {
-  const { label, variant, action, disabled } = element.props as {
-    label: string;
-    variant?: string | null;
-    action: { name: string };
-    disabled?: boolean | null;
-  };
-
-  const variants: Record<string, React.CSSProperties> = {
-    primary: { backgroundColor: '#6366f1', color: '#fff', border: 'none' },
-    secondary: { backgroundColor: '#fff', color: '#374151', border: '1px solid #d1d5db' },
-    danger: { backgroundColor: '#dc2626', color: '#fff', border: 'none' },
-    ghost: { backgroundColor: 'transparent', color: '#6b7280', border: 'none' },
-  };
-
-  const handleClick = () => {
-    if (!disabled && action && onAction) {
-      onAction(action);
-    }
-  };
-
-  return (
-    <button
-      onClick={handleClick}
-      disabled={!!disabled || loading}
-      style={{
-        padding: '8px 16px',
-        borderRadius: '6px',
-        fontSize: '14px',
-        fontWeight: 500,
-        cursor: disabled ? 'not-allowed' : 'pointer',
-        opacity: disabled ? 0.5 : 1,
-        ...variants[variant || 'primary'],
-      }}
-    >
-      {loading ? 'Loading...' : label}
-    </button>
-  );
-}
-
-// Alert component
-export function Alert({ element }: ComponentRenderProps) {
-  const { message, variant, dismissible } = element.props as {
-    message: string | { path: string };
-    variant?: string | null;
-    dismissible?: boolean | null;
-  };
-
-  const resolvedMessage = useResolvedValue(message);
-
-  const defaultStyle = { bg: '#eff6ff', border: '#bfdbfe', text: '#1e40af' };
-  const variants: Record<string, { bg: string; border: string; text: string }> = {
-    info: defaultStyle,
-    success: { bg: '#d1fae5', border: '#a7f3d0', text: '#065f46' },
-    warning: { bg: '#fef3c7', border: '#fcd34d', text: '#92400e' },
-    error: { bg: '#fee2e2', border: '#fca5a5', text: '#991b1b' },
-  };
-
-  const style = variants[variant || 'info'] ?? defaultStyle;
-
-  return (
-    <div
-      style={{
-        padding: '12px 16px',
-        borderRadius: '8px',
-        backgroundColor: style.bg,
-        border: `1px solid ${style.border}`,
-        color: style.text,
-        fontSize: '14px',
-      }}
-    >
-      {resolvedMessage}
-    </div>
-  );
-}
-
-// Grid component
-export function Grid({ element, children }: ComponentRenderProps) {
-  const { columns, gap } = element.props as {
-    columns?: number | null;
-    gap?: string | null;
-  };
-
-  const gaps: Record<string, string> = {
-    none: '0',
-    sm: '8px',
-    md: '16px',
-    lg: '24px',
-  };
-
-  return (
-    <div
-      style={{
-        display: 'grid',
-        gridTemplateColumns: `repeat(${columns || 2}, 1fr)`,
-        gap: gaps[gap || 'md'],
-      }}
-    >
-      {children}
-    </div>
-  );
-}
-
-// Stack component
-export function Stack({ element, children }: ComponentRenderProps) {
-  const { direction, gap, align } = element.props as {
-    direction?: string | null;
-    gap?: string | null;
-    align?: string | null;
-  };
-
-  const gaps: Record<string, string> = {
-    none: '0',
-    sm: '8px',
-    md: '16px',
-    lg: '24px',
-  };
-
-  const alignments: Record<string, string> = {
-    start: 'flex-start',
-    center: 'center',
-    end: 'flex-end',
-    stretch: 'stretch',
-  };
-
-  return (
-    <div
-      style={{
-        display: 'flex',
-        flexDirection: direction === 'horizontal' ? 'row' : 'column',
-        gap: gaps[gap || 'md'],
-        alignItems: alignments[align || 'stretch'],
-      }}
-    >
-      {children}
-    </div>
-  );
-}
-
-// Divider component
-export function Divider({ element }: ComponentRenderProps) {
-  const { orientation } = element.props as {
-    orientation?: string | null;
-  };
-
-  if (orientation === 'vertical') {
-    return (
-      <div
-        style={{
-          width: '1px',
-          backgroundColor: '#e5e7eb',
-          alignSelf: 'stretch',
-        }}
-      />
-    );
-  }
-
-  return (
-    <hr
-      style={{
-        border: 'none',
-        borderTop: '1px solid #e5e7eb',
-        margin: '16px 0',
-      }}
-    />
-  );
-}
-
-// Badge component
-export function Badge({ element }: ComponentRenderProps) {
-  const { text, variant } = element.props as {
-    text: string | { path: string };
-    variant?: string | null;
-  };
-
-  const resolvedText = useResolvedValue(text);
-
-  const defaultBadgeStyle = { bg: '#f3f4f6', text: '#374151' };
-  const variants: Record<string, { bg: string; text: string }> = {
-    default: defaultBadgeStyle,
-    success: { bg: '#d1fae5', text: '#065f46' },
-    warning: { bg: '#fef3c7', text: '#92400e' },
-    error: { bg: '#fee2e2', text: '#991b1b' },
-    info: { bg: '#dbeafe', text: '#1e40af' },
-  };
-
-  const style = variants[variant || 'default'] ?? defaultBadgeStyle;
-
-  return (
-    <span
-      style={{
-        display: 'inline-block',
-        padding: '2px 8px',
-        borderRadius: '12px',
-        fontSize: '12px',
-        fontWeight: 500,
-        backgroundColor: style.bg,
-        color: style.text,
-      }}
-    >
-      {resolvedText}
-    </span>
-  );
-}
-
-// TextField component
-export function TextField({ element }: ComponentRenderProps) {
-  const { label, valuePath, placeholder, type, checks, validateOn } = element.props as {
-    label: string;
-    valuePath: string;
-    placeholder?: string | null;
-    type?: string | null;
-    checks?: Array<{ fn: string; message: string }> | null;
-    validateOn?: string | null;
-  };
-
-  const { data, set } = useData();
-  const value = getByPath(data, valuePath) as string | undefined;
-  
-  const { errors, validate, touch } = useFieldValidation(valuePath, {
-    checks: checks ?? undefined,
-    validateOn: (validateOn as 'change' | 'blur' | 'submit') ?? 'blur',
-  });
-
-  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-    set(valuePath, e.target.value);
-    if (validateOn === 'change') {
-      validate();
-    }
-  };
-
-  const handleBlur = () => {
-    touch();
-    if (validateOn === 'blur' || !validateOn) {
-      validate();
-    }
-  };
-
-  return (
-    <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
-      <label style={{ fontSize: '14px', fontWeight: 500, color: '#374151' }}>
-        {label}
-      </label>
-      <input
-        type={type || 'text'}
-        value={value ?? ''}
-        onChange={handleChange}
-        onBlur={handleBlur}
-        placeholder={placeholder ?? ''}
-        style={{
-          padding: '8px 12px',
-          borderRadius: '6px',
-          border: errors.length > 0 ? '1px solid #ef4444' : '1px solid #d1d5db',
-          fontSize: '14px',
-          outline: 'none',
-        }}
-      />
-      {errors.map((error, i) => (
-        <span key={i} style={{ fontSize: '12px', color: '#ef4444' }}>
-          {error}
-        </span>
-      ))}
-    </div>
-  );
-}
-
-// Select component
-export function Select({ element }: ComponentRenderProps) {
-  const { label, valuePath, options, placeholder } = element.props as {
-    label: string;
-    valuePath: string;
-    options: Array<{ value: string; label: string }>;
-    placeholder?: string | null;
-  };
-
-  const { data, set } = useData();
-  const value = getByPath(data, valuePath) as string | undefined;
-
-  return (
-    <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
-      <label style={{ fontSize: '14px', fontWeight: 500, color: '#374151' }}>
-        {label}
-      </label>
-      <select
-        value={value ?? ''}
-        onChange={(e) => set(valuePath, e.target.value)}
-        style={{
-          padding: '8px 12px',
-          borderRadius: '6px',
-          border: '1px solid #d1d5db',
-          fontSize: '14px',
-          outline: 'none',
-          backgroundColor: '#fff',
-        }}
-      >
-        {placeholder && <option value="">{placeholder}</option>}
-        {options.map((opt) => (
-          <option key={opt.value} value={opt.value}>
-            {opt.label}
-          </option>
-        ))}
-      </select>
-    </div>
-  );
-}
-
-// DatePicker component (simplified)
-export function DatePicker({ element }: ComponentRenderProps) {
-  const { label, valuePath } = element.props as {
-    label: string;
-    valuePath: string;
-  };
-
-  const { data, set } = useData();
-  const value = getByPath(data, valuePath) as string | undefined;
-
-  return (
-    <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
-      <label style={{ fontSize: '14px', fontWeight: 500, color: '#374151' }}>
-        {label}
-      </label>
-      <input
-        type="date"
-        value={value ?? ''}
-        onChange={(e) => set(valuePath, e.target.value)}
-        style={{
-          padding: '8px 12px',
-          borderRadius: '6px',
-          border: '1px solid #d1d5db',
-          fontSize: '14px',
-          outline: 'none',
-        }}
-      />
-    </div>
-  );
-}
-
-// Empty component
-export function Empty({ element }: ComponentRenderProps) {
-  const { icon, title, description } = element.props as {
-    icon?: string | null;
-    title: string;
-    description?: string | null;
-  };
-
-  return (
-    <div style={{ textAlign: 'center', padding: '40px 20px' }}>
-      {icon && <div style={{ fontSize: '48px', marginBottom: '16px' }}>{icon}</div>}
-      <h3 style={{ margin: '0 0 8px', fontSize: '16px', fontWeight: 600, color: '#111827' }}>
-        {title}
-      </h3>
-      {description && (
-        <p style={{ margin: 0, fontSize: '14px', color: '#6b7280' }}>{description}</p>
-      )}
-    </div>
-  );
-}
-
-// List component
-export function List({ element, children }: ComponentRenderProps) {
-  const { dataPath } = element.props as {
-    dataPath: string;
-    itemKey?: string | null;
-  };
-
-  const { data } = useData();
-  const listData = getByPath(data, dataPath) as Array<unknown> | undefined;
-
-  if (!listData || !Array.isArray(listData)) {
-    return <div style={{ color: '#6b7280' }}>No items</div>;
-  }
-
-  // For now, just render children
-  // In a full implementation, children would be cloned for each item
-  return <div>{children}</div>;
-}
-
-// Export all components as a registry
-export const componentRegistry = {
-  Card,
-  Heading,
-  Text,
-  Metric,
-  Chart,
-  Table,
-  Button,
-  Alert,
-  Grid,
-  Stack,
-  Divider,
-  Badge,
-  TextField,
-  Select,
-  DatePicker,
-  Empty,
-  List,
-};

+ 0 - 192
apps/dashboard/lib/catalog.ts

@@ -1,192 +0,0 @@
-import { createCatalog } from '@json-render/core';
-import { z } from 'zod';
-
-/**
- * Dashboard component catalog
- *
- * This defines the ONLY components that the AI can generate.
- * It acts as a guardrail - the AI cannot create arbitrary HTML/CSS.
- * 
- * Note: OpenAI structured output requires all fields to be required.
- * Use .nullable() instead of .optional() for optional fields.
- */
-export const dashboardCatalog = createCatalog({
-  name: 'dashboard',
-  components: {
-    // Layout Components
-    Card: {
-      props: z.object({
-        title: z.string().nullable(),
-        description: z.string().nullable(),
-        padding: z.enum(['sm', 'md', 'lg']).nullable(),
-      }),
-      hasChildren: true,
-      description: 'A card container with optional title',
-    },
-
-    Grid: {
-      props: z.object({
-        columns: z.number().min(1).max(4).nullable(),
-        gap: z.enum(['sm', 'md', 'lg']).nullable(),
-      }),
-      hasChildren: true,
-      description: 'Grid layout with configurable columns',
-    },
-
-    Stack: {
-      props: z.object({
-        direction: z.enum(['horizontal', 'vertical']).nullable(),
-        gap: z.enum(['sm', 'md', 'lg']).nullable(),
-        align: z.enum(['start', 'center', 'end', 'stretch']).nullable(),
-      }),
-      hasChildren: true,
-      description: 'Flex stack for horizontal or vertical layouts',
-    },
-
-    // Data Display Components
-    Metric: {
-      props: z.object({
-        label: z.string(),
-        valuePath: z.string(),
-        format: z.enum(['number', 'currency', 'percent']).nullable(),
-        trend: z.enum(['up', 'down', 'neutral']).nullable(),
-        trendValue: z.string().nullable(),
-      }),
-      description: 'Display a single metric with optional trend indicator',
-    },
-
-    Chart: {
-      props: z.object({
-        type: z.enum(['bar', 'line', 'pie', 'area']),
-        dataPath: z.string(),
-        title: z.string().nullable(),
-        height: z.number().nullable(),
-      }),
-      description: 'Display a chart from array data',
-    },
-
-    Table: {
-      props: z.object({
-        dataPath: z.string(),
-        columns: z.array(
-          z.object({
-            key: z.string(),
-            label: z.string(),
-            format: z.enum(['text', 'currency', 'date', 'badge']).nullable(),
-          })
-        ),
-      }),
-      description: 'Display tabular data',
-    },
-
-    List: {
-      props: z.object({
-        dataPath: z.string(),
-        emptyMessage: z.string().nullable(),
-      }),
-      hasChildren: true,
-      description: 'Render a list from array data',
-    },
-
-    // Interactive Components
-    Button: {
-      props: z.object({
-        label: z.string(),
-        variant: z.enum(['primary', 'secondary', 'danger', 'ghost']).nullable(),
-        size: z.enum(['sm', 'md', 'lg']).nullable(),
-        action: z.string(),
-        disabled: z.boolean().nullable(),
-      }),
-      description: 'Clickable button with action',
-    },
-
-    Select: {
-      props: z.object({
-        label: z.string().nullable(),
-        bindPath: z.string(),
-        options: z.array(
-          z.object({
-            value: z.string(),
-            label: z.string(),
-          })
-        ),
-        placeholder: z.string().nullable(),
-      }),
-      description: 'Dropdown select input',
-    },
-
-    DatePicker: {
-      props: z.object({
-        label: z.string().nullable(),
-        bindPath: z.string(),
-        placeholder: z.string().nullable(),
-      }),
-      description: 'Date picker input',
-    },
-
-    // Typography
-    Heading: {
-      props: z.object({
-        text: z.string(),
-        level: z.enum(['h1', 'h2', 'h3', 'h4']).nullable(),
-      }),
-      description: 'Section heading',
-    },
-
-    Text: {
-      props: z.object({
-        content: z.string(),
-        variant: z.enum(['body', 'caption', 'label']).nullable(),
-        color: z.enum(['default', 'muted', 'success', 'warning', 'danger']).nullable(),
-      }),
-      description: 'Text paragraph',
-    },
-
-    // Status Components
-    Badge: {
-      props: z.object({
-        text: z.string(),
-        variant: z.enum(['default', 'success', 'warning', 'danger', 'info']).nullable(),
-      }),
-      description: 'Small status badge',
-    },
-
-    Alert: {
-      props: z.object({
-        type: z.enum(['info', 'success', 'warning', 'error']),
-        title: z.string(),
-        message: z.string().nullable(),
-        dismissible: z.boolean().nullable(),
-      }),
-      description: 'Alert/notification banner',
-    },
-
-    // Special Components
-    Divider: {
-      props: z.object({
-        label: z.string().nullable(),
-      }),
-      description: 'Visual divider',
-    },
-
-    Empty: {
-      props: z.object({
-        title: z.string(),
-        description: z.string().nullable(),
-        action: z.string().nullable(),
-        actionLabel: z.string().nullable(),
-      }),
-      description: 'Empty state placeholder',
-    },
-  },
-  actions: {
-    export_report: { description: 'Export the current dashboard to PDF' },
-    refresh_data: { description: 'Refresh all metrics and charts' },
-    view_details: { description: 'View detailed information' },
-    apply_filter: { description: 'Apply the current filter settings' },
-  },
-  validation: 'strict',
-});
-
-// Export the component list for the AI prompt
-export const componentList = dashboardCatalog.componentNames as string[];

+ 0 - 6
apps/dashboard/next.config.js

@@ -1,6 +0,0 @@
-/** @type {import('next').NextConfig} */
-const nextConfig = {
-  transpilePackages: ['@json-render/core', '@json-render/react'],
-};
-
-module.exports = nextConfig;

+ 0 - 27
apps/dashboard/package.json

@@ -1,27 +0,0 @@
-{
-  "name": "dashboard",
-  "version": "0.1.0",
-  "private": true,
-  "scripts": {
-    "dev": "next dev --turbopack --port 3001",
-    "build": "next build",
-    "start": "next start",
-    "lint": "next lint"
-  },
-  "dependencies": {
-    "@ai-sdk/gateway": "^3.0.13",
-    "ai": "^6.0.33",
-    "@json-render/core": "workspace:*",
-    "@json-render/react": "workspace:*",
-    "next": "^15.3.0",
-    "react": "^19.0.0",
-    "react-dom": "^19.0.0",
-    "zod": "^3.24.0"
-  },
-  "devDependencies": {
-    "@types/node": "^22.10.0",
-    "@types/react": "^19.0.0",
-    "@types/react-dom": "^19.0.0",
-    "typescript": "^5.7.2"
-  }
-}

+ 0 - 11
apps/dashboard/tsconfig.json

@@ -1,11 +0,0 @@
-{
-  "extends": "../../packages/typescript-config/nextjs.json",
-  "compilerOptions": {
-    "plugins": [{ "name": "next" }],
-    "paths": {
-      "@/*": ["./*"]
-    }
-  },
-  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
-  "exclude": ["node_modules"]
-}

+ 0 - 28
apps/docs/package.json

@@ -1,28 +0,0 @@
-{
-  "name": "docs",
-  "version": "0.1.0",
-  "type": "module",
-  "private": true,
-  "scripts": {
-    "dev": "next dev --port 3002",
-    "build": "next build",
-    "start": "next start",
-    "lint": "eslint --max-warnings 0",
-    "check-types": "next typegen && tsc --noEmit"
-  },
-  "dependencies": {
-    "@repo/ui": "workspace:*",
-    "next": "16.1.0",
-    "react": "^19.2.0",
-    "react-dom": "^19.2.0"
-  },
-  "devDependencies": {
-    "@repo/eslint-config": "workspace:*",
-    "@repo/typescript-config": "workspace:*",
-    "@types/node": "^22.15.3",
-    "@types/react": "19.2.2",
-    "@types/react-dom": "19.2.2",
-    "eslint": "^9.39.1",
-    "typescript": "5.9.2"
-  }
-}

+ 1 - 1
examples/dashboard/package.json

@@ -1,5 +1,5 @@
 {
-  "name": "dashboard",
+  "name": "example-dashboard",
   "version": "0.1.0",
   "private": true,
   "scripts": {

+ 0 - 77
pnpm-lock.yaml

@@ -18,83 +18,6 @@ importers:
         specifier: 5.9.2
         version: 5.9.2
 
-  apps/dashboard:
-    dependencies:
-      '@ai-sdk/gateway':
-        specifier: ^3.0.13
-        version: 3.0.13(zod@3.25.76)
-      '@json-render/core':
-        specifier: workspace:*
-        version: link:../../packages/core
-      '@json-render/react':
-        specifier: workspace:*
-        version: link:../../packages/react
-      ai:
-        specifier: ^6.0.33
-        version: 6.0.33(zod@3.25.76)
-      next:
-        specifier: ^15.3.0
-        version: 15.5.9(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
-      react:
-        specifier: ^19.0.0
-        version: 19.2.3
-      react-dom:
-        specifier: ^19.0.0
-        version: 19.2.3(react@19.2.3)
-      zod:
-        specifier: ^3.24.0
-        version: 3.25.76
-    devDependencies:
-      '@types/node':
-        specifier: ^22.10.0
-        version: 22.19.6
-      '@types/react':
-        specifier: ^19.0.0
-        version: 19.2.2
-      '@types/react-dom':
-        specifier: ^19.0.0
-        version: 19.2.2(@types/react@19.2.2)
-      typescript:
-        specifier: ^5.7.2
-        version: 5.9.2
-
-  apps/docs:
-    dependencies:
-      '@repo/ui':
-        specifier: workspace:*
-        version: link:../../packages/ui
-      next:
-        specifier: 16.1.0
-        version: 16.1.0(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
-      react:
-        specifier: ^19.2.0
-        version: 19.2.3
-      react-dom:
-        specifier: ^19.2.0
-        version: 19.2.3(react@19.2.3)
-    devDependencies:
-      '@repo/eslint-config':
-        specifier: workspace:*
-        version: link:../../packages/eslint-config
-      '@repo/typescript-config':
-        specifier: workspace:*
-        version: link:../../packages/typescript-config
-      '@types/node':
-        specifier: ^22.15.3
-        version: 22.19.6
-      '@types/react':
-        specifier: 19.2.2
-        version: 19.2.2
-      '@types/react-dom':
-        specifier: 19.2.2
-        version: 19.2.2(@types/react@19.2.2)
-      eslint:
-        specifier: ^9.39.1
-        version: 9.39.2(jiti@2.6.1)
-      typescript:
-        specifier: 5.9.2
-        version: 5.9.2
-
   apps/web:
     dependencies:
       '@ai-sdk/gateway':