catalog.ts 14 KB

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