| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- '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>
- );
- }
|