page.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. 'use client';
  2. import { useState, useCallback } from 'react';
  3. import {
  4. DataProvider,
  5. ActionProvider,
  6. VisibilityProvider,
  7. useUIStream,
  8. Renderer,
  9. } from '@json-render/react';
  10. import { componentRegistry } from '@/components/ui';
  11. const INITIAL_DATA = {
  12. analytics: {
  13. revenue: 125000,
  14. growth: 0.15,
  15. customers: 1234,
  16. orders: 567,
  17. salesByRegion: [
  18. { label: 'US', value: 45000 },
  19. { label: 'EU', value: 35000 },
  20. { label: 'Asia', value: 28000 },
  21. { label: 'Other', value: 17000 },
  22. ],
  23. recentTransactions: [
  24. { id: 'TXN001', customer: 'Acme Corp', amount: 1500, status: 'completed', date: '2024-01-15' },
  25. { id: 'TXN002', customer: 'Globex Inc', amount: 2300, status: 'pending', date: '2024-01-14' },
  26. { id: 'TXN003', customer: 'Initech', amount: 890, status: 'completed', date: '2024-01-13' },
  27. { id: 'TXN004', customer: 'Umbrella Co', amount: 4200, status: 'completed', date: '2024-01-12' },
  28. ],
  29. },
  30. form: {
  31. dateRange: '',
  32. region: '',
  33. },
  34. };
  35. const ACTION_HANDLERS = {
  36. export_report: () => alert('Exporting report...'),
  37. refresh_data: () => alert('Refreshing data...'),
  38. view_details: (params: Record<string, unknown>) => alert(`Details: ${JSON.stringify(params)}`),
  39. apply_filter: () => alert('Applying filters...'),
  40. };
  41. function DashboardContent() {
  42. const [prompt, setPrompt] = useState('');
  43. const { tree, isStreaming, error, send, clear } = useUIStream({
  44. api: '/api/generate',
  45. onError: (err) => console.error('Generation error:', err),
  46. });
  47. const handleSubmit = useCallback(
  48. async (e: React.FormEvent) => {
  49. e.preventDefault();
  50. if (!prompt.trim()) return;
  51. await send(prompt, { data: INITIAL_DATA });
  52. },
  53. [prompt, send]
  54. );
  55. const examples = [
  56. 'Revenue dashboard with metrics and chart',
  57. 'Recent transactions table',
  58. 'Customer count with trend',
  59. ];
  60. const hasElements = tree && Object.keys(tree.elements).length > 0;
  61. return (
  62. <div style={{ maxWidth: 960, margin: '0 auto', padding: '48px 24px' }}>
  63. <header style={{ marginBottom: 48 }}>
  64. <h1 style={{ margin: 0, fontSize: 32, fontWeight: 600, letterSpacing: '-0.02em' }}>
  65. Dashboard
  66. </h1>
  67. <p style={{ margin: '8px 0 0', color: 'var(--muted)', fontSize: 16 }}>
  68. Generate widgets from prompts. Constrained to your catalog.
  69. </p>
  70. </header>
  71. <form onSubmit={handleSubmit} style={{ marginBottom: 32 }}>
  72. <div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
  73. <input
  74. type="text"
  75. value={prompt}
  76. onChange={(e) => setPrompt(e.target.value)}
  77. placeholder="Describe what you want..."
  78. disabled={isStreaming}
  79. style={{
  80. flex: 1,
  81. padding: '12px 16px',
  82. background: 'var(--card)',
  83. border: '1px solid var(--border)',
  84. borderRadius: 'var(--radius)',
  85. color: 'var(--foreground)',
  86. fontSize: 15,
  87. outline: 'none',
  88. }}
  89. />
  90. <button
  91. type="submit"
  92. disabled={isStreaming || !prompt.trim()}
  93. style={{
  94. padding: '12px 24px',
  95. background: isStreaming ? 'var(--border)' : 'var(--foreground)',
  96. color: 'var(--background)',
  97. border: 'none',
  98. borderRadius: 'var(--radius)',
  99. fontSize: 15,
  100. fontWeight: 500,
  101. opacity: isStreaming || !prompt.trim() ? 0.5 : 1,
  102. }}
  103. >
  104. {isStreaming ? 'Generating...' : 'Generate'}
  105. </button>
  106. {hasElements && (
  107. <button
  108. type="button"
  109. onClick={clear}
  110. style={{
  111. padding: '12px 16px',
  112. background: 'transparent',
  113. color: 'var(--muted)',
  114. border: '1px solid var(--border)',
  115. borderRadius: 'var(--radius)',
  116. fontSize: 15,
  117. }}
  118. >
  119. Clear
  120. </button>
  121. )}
  122. </div>
  123. <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
  124. {examples.map((ex) => (
  125. <button
  126. key={ex}
  127. type="button"
  128. onClick={() => setPrompt(ex)}
  129. style={{
  130. padding: '6px 12px',
  131. background: 'var(--card)',
  132. color: 'var(--muted)',
  133. border: '1px solid var(--border)',
  134. borderRadius: 'var(--radius)',
  135. fontSize: 13,
  136. }}
  137. >
  138. {ex}
  139. </button>
  140. ))}
  141. </div>
  142. </form>
  143. {error && (
  144. <div
  145. style={{
  146. padding: 16,
  147. marginBottom: 24,
  148. background: 'var(--card)',
  149. border: '1px solid var(--border)',
  150. borderRadius: 'var(--radius)',
  151. color: '#ef4444',
  152. fontSize: 14,
  153. }}
  154. >
  155. {error.message}
  156. </div>
  157. )}
  158. <div
  159. style={{
  160. minHeight: 300,
  161. padding: 24,
  162. background: 'var(--card)',
  163. border: '1px solid var(--border)',
  164. borderRadius: 'var(--radius)',
  165. }}
  166. >
  167. {!hasElements && !isStreaming ? (
  168. <div style={{ textAlign: 'center', padding: '60px 20px', color: 'var(--muted)' }}>
  169. <p style={{ margin: 0 }}>Enter a prompt to generate a widget</p>
  170. </div>
  171. ) : tree ? (
  172. <Renderer tree={tree} registry={componentRegistry} loading={isStreaming} />
  173. ) : null}
  174. </div>
  175. {hasElements && (
  176. <details style={{ marginTop: 24 }}>
  177. <summary style={{ cursor: 'pointer', fontSize: 14, color: 'var(--muted)' }}>
  178. View JSON
  179. </summary>
  180. <pre
  181. style={{
  182. marginTop: 8,
  183. padding: 16,
  184. background: 'var(--card)',
  185. border: '1px solid var(--border)',
  186. borderRadius: 'var(--radius)',
  187. overflow: 'auto',
  188. fontSize: 12,
  189. color: 'var(--muted)',
  190. }}
  191. >
  192. {JSON.stringify(tree, null, 2)}
  193. </pre>
  194. </details>
  195. )}
  196. </div>
  197. );
  198. }
  199. export default function DashboardPage() {
  200. return (
  201. <DataProvider initialData={INITIAL_DATA}>
  202. <VisibilityProvider>
  203. <ActionProvider handlers={ACTION_HANDLERS}>
  204. <DashboardContent />
  205. </ActionProvider>
  206. </VisibilityProvider>
  207. </DataProvider>
  208. );
  209. }