catalog.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. import { defineCatalog } from "@json-render/core";
  2. import { schema } from "@json-render/react/schema";
  3. import { z } from "zod";
  4. // =============================================================================
  5. // Shared 3D schemas
  6. // =============================================================================
  7. const vec3 = z.array(z.number());
  8. const animation3D = z
  9. .object({
  10. rotate: vec3.nullable(),
  11. })
  12. .nullable();
  13. const transform3DProps = {
  14. position: vec3.nullable(),
  15. rotation: vec3.nullable(),
  16. scale: vec3.nullable(),
  17. };
  18. const material3DProps = {
  19. color: z.string().nullable(),
  20. metalness: z.number().nullable(),
  21. roughness: z.number().nullable(),
  22. emissive: z.string().nullable(),
  23. emissiveIntensity: z.number().nullable(),
  24. wireframe: z.boolean().nullable(),
  25. opacity: z.number().nullable(),
  26. };
  27. const mesh3DProps = {
  28. ...transform3DProps,
  29. ...material3DProps,
  30. args: z.array(z.number()).nullable(),
  31. animation: animation3D,
  32. };
  33. /**
  34. * json-render + AI SDK Example Catalog
  35. *
  36. * Components for rendering data dashboards generated by the ToolLoopAgent.
  37. * Data flows in through tools (weather, GitHub, crypto, HN), not user actions.
  38. */
  39. export const explorerCatalog = defineCatalog(schema, {
  40. components: {
  41. // Layout
  42. Stack: {
  43. props: z.object({
  44. direction: z.enum(["horizontal", "vertical"]).nullable(),
  45. gap: z.enum(["sm", "md", "lg"]).nullable(),
  46. wrap: z.boolean().nullable(),
  47. }),
  48. slots: ["default"],
  49. description: "Flex layout container",
  50. example: { direction: "vertical", gap: "md", wrap: null },
  51. },
  52. Card: {
  53. props: z.object({
  54. title: z.string().nullable(),
  55. description: z.string().nullable(),
  56. }),
  57. slots: ["default"],
  58. description: "Card container with optional title and description",
  59. example: { title: "Weather", description: "Current conditions" },
  60. },
  61. Grid: {
  62. props: z.object({
  63. columns: z.enum(["1", "2", "3", "4"]).nullable(),
  64. gap: z.enum(["sm", "md", "lg"]).nullable(),
  65. }),
  66. slots: ["default"],
  67. description: "Responsive grid layout container",
  68. example: { columns: "3", gap: "md" },
  69. },
  70. // Typography
  71. Heading: {
  72. props: z.object({
  73. text: z.string(),
  74. level: z.enum(["h1", "h2", "h3", "h4"]).nullable(),
  75. }),
  76. description: "Section heading",
  77. example: { text: "Data Explorer", level: "h1" },
  78. },
  79. Text: {
  80. props: z.object({
  81. content: z.string(),
  82. muted: z.boolean().nullable(),
  83. }),
  84. description: "Text content",
  85. example: { content: "Here is your data overview." },
  86. },
  87. // Data display
  88. Badge: {
  89. props: z.object({
  90. text: z.string(),
  91. variant: z
  92. .enum(["default", "secondary", "destructive", "outline"])
  93. .nullable(),
  94. }),
  95. description: "Status badge",
  96. example: { text: "Live", variant: "default" },
  97. },
  98. Alert: {
  99. props: z.object({
  100. variant: z.enum(["default", "destructive"]).nullable(),
  101. title: z.string(),
  102. description: z.string().nullable(),
  103. }),
  104. description: "Alert or info message",
  105. },
  106. Separator: {
  107. props: z.object({}),
  108. description: "Visual divider",
  109. },
  110. Metric: {
  111. props: z.object({
  112. label: z.string(),
  113. value: z.string(),
  114. detail: z.string().nullable(),
  115. trend: z.enum(["up", "down", "neutral"]).nullable(),
  116. }),
  117. description:
  118. "Single metric display with label, value, and optional trend indicator",
  119. example: {
  120. label: "Temperature",
  121. value: "72F",
  122. detail: "Feels like 68F",
  123. trend: "up",
  124. },
  125. },
  126. Table: {
  127. props: z.object({
  128. data: z.array(z.record(z.string(), z.unknown())),
  129. columns: z.array(
  130. z.object({
  131. key: z.string(),
  132. label: z.string(),
  133. }),
  134. ),
  135. emptyMessage: z.string().nullable(),
  136. }),
  137. description:
  138. 'Data table. Use { "$state": "/path" } to bind read-only data from state.',
  139. example: {
  140. data: { $state: "/stories" },
  141. columns: [
  142. { key: "title", label: "Title" },
  143. { key: "score", label: "Score" },
  144. ],
  145. },
  146. },
  147. Link: {
  148. props: z.object({
  149. text: z.string(),
  150. href: z.string(),
  151. }),
  152. description: "External link that opens in a new tab",
  153. example: { text: "View on GitHub", href: "https://github.com" },
  154. },
  155. // Charts
  156. BarChart: {
  157. props: z.object({
  158. title: z.string().nullable(),
  159. data: z.array(z.record(z.string(), z.unknown())),
  160. xKey: z.string(),
  161. yKey: z.string(),
  162. aggregate: z.enum(["sum", "count", "avg"]).nullable(),
  163. color: z.string().nullable(),
  164. height: z.number().nullable(),
  165. }),
  166. description:
  167. 'Bar chart visualization. Use { "$state": "/path" } to bind read-only data. xKey is the category field, yKey is the numeric value field.',
  168. },
  169. LineChart: {
  170. props: z.object({
  171. title: z.string().nullable(),
  172. data: z.array(z.record(z.string(), z.unknown())),
  173. xKey: z.string(),
  174. yKey: z.string(),
  175. aggregate: z.enum(["sum", "count", "avg"]).nullable(),
  176. color: z.string().nullable(),
  177. height: z.number().nullable(),
  178. }),
  179. description:
  180. 'Line chart visualization. Use { "$state": "/path" } to bind read-only data. xKey is the x-axis field, yKey is the numeric value field.',
  181. },
  182. // Interactive
  183. Tabs: {
  184. props: z.object({
  185. defaultValue: z.string().nullable(),
  186. tabs: z.array(
  187. z.object({
  188. value: z.string(),
  189. label: z.string(),
  190. }),
  191. ),
  192. }),
  193. slots: ["default"],
  194. description: "Tabbed content container",
  195. },
  196. TabContent: {
  197. props: z.object({
  198. value: z.string(),
  199. }),
  200. slots: ["default"],
  201. description: "Content for a specific tab",
  202. },
  203. Progress: {
  204. props: z.object({
  205. value: z.number(),
  206. max: z.number().nullable(),
  207. }),
  208. description: "Progress bar",
  209. },
  210. Skeleton: {
  211. props: z.object({
  212. width: z.string().nullable(),
  213. height: z.string().nullable(),
  214. }),
  215. description: "Loading placeholder",
  216. },
  217. // Educational / Rich content
  218. Callout: {
  219. props: z.object({
  220. type: z.enum(["info", "tip", "warning", "important"]).nullable(),
  221. title: z.string().nullable(),
  222. content: z.string(),
  223. }),
  224. description:
  225. "Highlighted callout box for tips, warnings, notes, or key information",
  226. example: {
  227. type: "tip",
  228. title: "Did you know?",
  229. content: "The sun is about 93 million miles from Earth.",
  230. },
  231. },
  232. Accordion: {
  233. props: z.object({
  234. items: z.array(
  235. z.object({
  236. title: z.string(),
  237. content: z.string(),
  238. }),
  239. ),
  240. type: z.enum(["single", "multiple"]).nullable(),
  241. }),
  242. description:
  243. "Collapsible accordion sections for organizing detailed content",
  244. example: {
  245. items: [{ title: "Overview", content: "A brief introduction." }],
  246. type: "multiple",
  247. },
  248. },
  249. Timeline: {
  250. props: z.object({
  251. items: z.array(
  252. z.object({
  253. title: z.string(),
  254. description: z.string().nullable(),
  255. date: z.string().nullable(),
  256. status: z.enum(["completed", "current", "upcoming"]).nullable(),
  257. }),
  258. ),
  259. }),
  260. description:
  261. "Vertical timeline showing ordered events, steps, or historical milestones",
  262. example: {
  263. items: [
  264. {
  265. title: "Discovery",
  266. description: "Initial breakthrough",
  267. date: "1905",
  268. status: "completed",
  269. },
  270. ],
  271. },
  272. },
  273. PieChart: {
  274. props: z.object({
  275. title: z.string().nullable(),
  276. data: z.array(z.record(z.string(), z.unknown())),
  277. nameKey: z.string(),
  278. valueKey: z.string(),
  279. height: z.number().nullable(),
  280. }),
  281. description:
  282. 'Pie/donut chart for proportional data. Use { "$state": "/path" } to bind read-only data. nameKey is the label field, valueKey is the numeric value field.',
  283. },
  284. // Interactive / Input
  285. RadioGroup: {
  286. props: z.object({
  287. label: z.string().nullable(),
  288. value: z.string().nullable(),
  289. options: z.array(
  290. z.object({
  291. value: z.string(),
  292. label: z.string(),
  293. }),
  294. ),
  295. }),
  296. description:
  297. 'Radio button group for single selection. Use { "$bindState": "/path" } for two-way binding. Use for multiple-choice questions, settings, or any single-select input.',
  298. example: {
  299. label: "Choose one",
  300. value: { $bindState: "/answer" },
  301. options: [
  302. { value: "a", label: "Option A" },
  303. { value: "b", label: "Option B" },
  304. ],
  305. },
  306. },
  307. SelectInput: {
  308. props: z.object({
  309. label: z.string().nullable(),
  310. value: z.string().nullable(),
  311. placeholder: z.string().nullable(),
  312. options: z.array(
  313. z.object({
  314. value: z.string(),
  315. label: z.string(),
  316. }),
  317. ),
  318. }),
  319. description:
  320. 'Dropdown select input. Use { "$bindState": "/path" } for two-way binding. Use when there are many options and a dropdown is more compact than radio buttons.',
  321. example: {
  322. label: "Country",
  323. value: { $bindState: "/selectedCountry" },
  324. placeholder: "Select a country",
  325. options: [
  326. { value: "us", label: "United States" },
  327. { value: "uk", label: "United Kingdom" },
  328. ],
  329. },
  330. },
  331. TextInput: {
  332. props: z.object({
  333. label: z.string().nullable(),
  334. value: z.string().nullable(),
  335. placeholder: z.string().nullable(),
  336. type: z.enum(["text", "email", "number", "password", "url"]).nullable(),
  337. }),
  338. description:
  339. 'Text input field. Use { "$bindState": "/path" } for two-way binding. Use for free-text entry like names, emails, search, etc.',
  340. example: {
  341. label: "Your name",
  342. value: { $bindState: "/userName" },
  343. placeholder: "Enter your name",
  344. type: "text",
  345. },
  346. },
  347. Button: {
  348. props: z.object({
  349. label: z.string(),
  350. variant: z
  351. .enum(["default", "secondary", "destructive", "outline", "ghost"])
  352. .nullable(),
  353. size: z.enum(["default", "sm", "lg"]).nullable(),
  354. disabled: z.boolean().nullable(),
  355. }),
  356. description:
  357. "Clickable button. Use with on.press to trigger actions like setState, pushState, etc. Can be used for quiz submissions, form actions, navigation, and more.",
  358. example: {
  359. label: "Submit",
  360. variant: "default",
  361. size: "default",
  362. disabled: null,
  363. },
  364. },
  365. // =========================================================================
  366. // 3D Scene Components (React Three Fiber)
  367. // =========================================================================
  368. // Containers
  369. Scene3D: {
  370. props: z.object({
  371. height: z.string().nullable(),
  372. background: z.string().nullable(),
  373. cameraPosition: vec3.nullable(),
  374. cameraFov: z.number().nullable(),
  375. autoRotate: z.boolean().nullable(),
  376. }),
  377. slots: ["default"],
  378. description:
  379. "3D scene container with orbit controls. All 3D components (Sphere, Box, lights, etc.) must be children of a Scene3D. height is a CSS value like '500px'.",
  380. example: {
  381. height: "500px",
  382. background: "#000010",
  383. cameraPosition: [0, 25, 45],
  384. cameraFov: null,
  385. autoRotate: null,
  386. },
  387. },
  388. Group3D: {
  389. props: z.object({
  390. ...transform3DProps,
  391. animation: animation3D,
  392. }),
  393. slots: ["default"],
  394. description:
  395. "3D group for positioning, rotating, and animating children together. Use to create orbits: position a planet inside a Group3D and animate the group's rotation.",
  396. example: {
  397. position: null,
  398. rotation: null,
  399. scale: null,
  400. animation: { rotate: [0, 0.005, 0] },
  401. },
  402. },
  403. // Geometry primitives
  404. Box: {
  405. props: z.object(mesh3DProps),
  406. description:
  407. "3D box/cube mesh. args: [width, height, depth]. Supports on.press for click interaction.",
  408. example: {
  409. position: [0, 0, 0],
  410. color: "#4488ff",
  411. args: [1, 1, 1],
  412. },
  413. },
  414. Sphere: {
  415. props: z.object(mesh3DProps),
  416. description:
  417. "3D sphere mesh. args: [radius, widthSegments, heightSegments]. Use higher segment counts (32+) for smooth spheres.",
  418. example: {
  419. position: [0, 0, 0],
  420. color: "#4B7BE5",
  421. args: [1, 32, 32],
  422. },
  423. },
  424. Cylinder: {
  425. props: z.object(mesh3DProps),
  426. description:
  427. "3D cylinder mesh. args: [radiusTop, radiusBottom, height, radialSegments].",
  428. example: {
  429. position: [0, 0, 0],
  430. color: "#88aa44",
  431. args: [1, 1, 2, 32],
  432. },
  433. },
  434. Cone: {
  435. props: z.object(mesh3DProps),
  436. description: "3D cone mesh. args: [radius, height, radialSegments].",
  437. example: {
  438. position: [0, 0, 0],
  439. color: "#ff8844",
  440. args: [1, 2, 32],
  441. },
  442. },
  443. Torus: {
  444. props: z.object(mesh3DProps),
  445. description:
  446. "3D torus (donut) mesh. args: [radius, tube, radialSegments, tubularSegments].",
  447. example: {
  448. position: [0, 0, 0],
  449. color: "#aa44ff",
  450. args: [1, 0.4, 16, 100],
  451. },
  452. },
  453. Plane: {
  454. props: z.object(mesh3DProps),
  455. description:
  456. "3D flat plane mesh. args: [width, height]. Useful for ground planes or flat surfaces.",
  457. example: {
  458. position: [0, -1, 0],
  459. rotation: [-Math.PI / 2, 0, 0],
  460. color: "#334455",
  461. args: [10, 10],
  462. },
  463. },
  464. Ring: {
  465. props: z.object(mesh3DProps),
  466. description:
  467. "3D flat ring mesh. args: [innerRadius, outerRadius, thetaSegments]. Great for orbit path indicators.",
  468. example: {
  469. position: [0, 0, 0],
  470. rotation: [-Math.PI / 2, 0, 0],
  471. color: "#ffffff",
  472. opacity: 0.2,
  473. args: [14.8, 15.2, 64],
  474. },
  475. },
  476. // Lights
  477. AmbientLight: {
  478. props: z.object({
  479. color: z.string().nullable(),
  480. intensity: z.number().nullable(),
  481. }),
  482. description:
  483. "Ambient light that illuminates all objects equally. Use for base scene illumination.",
  484. example: { color: null, intensity: 0.3 },
  485. },
  486. PointLight: {
  487. props: z.object({
  488. position: vec3.nullable(),
  489. color: z.string().nullable(),
  490. intensity: z.number().nullable(),
  491. distance: z.number().nullable(),
  492. }),
  493. description:
  494. "Point light that emits from a position in all directions. Use for suns, lamps, etc.",
  495. example: { position: [0, 0, 0], intensity: 2 },
  496. },
  497. DirectionalLight: {
  498. props: z.object({
  499. position: vec3.nullable(),
  500. color: z.string().nullable(),
  501. intensity: z.number().nullable(),
  502. }),
  503. description:
  504. "Directional light like sunlight. Position sets direction, not location.",
  505. example: { position: [5, 10, 5], intensity: 1 },
  506. },
  507. // Helpers (drei)
  508. Stars: {
  509. props: z.object({
  510. radius: z.number().nullable(),
  511. depth: z.number().nullable(),
  512. count: z.number().nullable(),
  513. factor: z.number().nullable(),
  514. fade: z.boolean().nullable(),
  515. speed: z.number().nullable(),
  516. }),
  517. description:
  518. "Starfield background for space scenes. Renders thousands of tiny points around the scene.",
  519. example: { count: 5000, fade: true },
  520. },
  521. Label3D: {
  522. props: z.object({
  523. text: z.string(),
  524. position: vec3.nullable(),
  525. rotation: vec3.nullable(),
  526. color: z.string().nullable(),
  527. fontSize: z.number().nullable(),
  528. anchorX: z.enum(["left", "center", "right"]).nullable(),
  529. anchorY: z.enum(["top", "middle", "bottom"]).nullable(),
  530. }),
  531. description:
  532. "Text label rendered in 3D space. Always faces the camera (billboard). Use for labeling objects in a scene.",
  533. example: {
  534. text: "Earth",
  535. position: [15, 2, 0],
  536. color: "#ffffff",
  537. fontSize: 0.8,
  538. },
  539. },
  540. },
  541. actions: {},
  542. });