page.tsx 6.9 KB

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