@gram-ai/elements 1.25.2 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/dist/components/Chat/stories/Charts.stories.d.ts +37 -0
  2. package/dist/components/Chat/stories/GenerativeUI.stories.d.ts +17 -0
  3. package/dist/components/ui/button.d.ts +1 -1
  4. package/dist/components/ui/buttonVariants.d.ts +1 -1
  5. package/dist/components/ui/charts.stories.d.ts +43 -0
  6. package/dist/components/ui/generative-ui.stories.d.ts +53 -0
  7. package/dist/elements.cjs +1 -1
  8. package/dist/elements.css +1 -1
  9. package/dist/elements.js +6 -6
  10. package/dist/index-BJnv49-A.js +37057 -0
  11. package/dist/index-BJnv49-A.js.map +1 -0
  12. package/dist/index-BpJstUh1.cjs +280 -0
  13. package/dist/index-BpJstUh1.cjs.map +1 -0
  14. package/dist/index-CUitXazZ.js +30426 -0
  15. package/dist/index-CUitXazZ.js.map +1 -0
  16. package/dist/index-ChW-CSuu.cjs +147 -0
  17. package/dist/index-ChW-CSuu.cjs.map +1 -0
  18. package/dist/plugins/chart/catalog.d.ts +123 -0
  19. package/dist/plugins/chart/index.d.ts +1 -1
  20. package/dist/plugins/chart/ui/area-chart.d.ts +16 -0
  21. package/dist/plugins/chart/ui/bar-chart.d.ts +16 -0
  22. package/dist/plugins/chart/ui/donut-chart.d.ts +17 -0
  23. package/dist/plugins/chart/ui/index.d.ts +7 -0
  24. package/dist/plugins/chart/ui/line-chart.d.ts +17 -0
  25. package/dist/plugins/chart/ui/pie-chart.d.ts +15 -0
  26. package/dist/plugins/chart/ui/radar-chart.d.ts +14 -0
  27. package/dist/plugins/chart/ui/scatter-chart.d.ts +18 -0
  28. package/dist/plugins/components/MacOSWindowFrame.d.ts +13 -0
  29. package/dist/plugins/components/PluginLoadingState.d.ts +1 -1
  30. package/dist/plugins/generative-ui/catalog.d.ts +293 -0
  31. package/dist/plugins/generative-ui/ui/accordion-wrapper.d.ts +18 -0
  32. package/dist/plugins/generative-ui/ui/accordion.d.ts +7 -0
  33. package/dist/plugins/generative-ui/ui/action-button.d.ts +10 -0
  34. package/dist/plugins/generative-ui/ui/alert-wrapper.d.ts +9 -0
  35. package/dist/plugins/generative-ui/ui/alert.d.ts +9 -0
  36. package/dist/plugins/generative-ui/ui/avatar-wrapper.d.ts +9 -0
  37. package/dist/plugins/generative-ui/ui/avatar.d.ts +11 -0
  38. package/dist/plugins/generative-ui/ui/badge.d.ts +12 -0
  39. package/dist/plugins/generative-ui/ui/button-wrapper.d.ts +15 -0
  40. package/dist/plugins/generative-ui/ui/button.d.ts +10 -0
  41. package/dist/plugins/generative-ui/ui/card-wrapper.d.ts +10 -0
  42. package/dist/plugins/generative-ui/ui/card.d.ts +9 -0
  43. package/dist/plugins/generative-ui/ui/checkbox-wrapper.d.ts +10 -0
  44. package/dist/plugins/generative-ui/ui/checkbox.d.ts +4 -0
  45. package/dist/plugins/generative-ui/ui/data-table.d.ts +10 -0
  46. package/dist/plugins/generative-ui/ui/dialog.d.ts +17 -0
  47. package/dist/plugins/generative-ui/ui/dropdown-menu.d.ts +25 -0
  48. package/dist/plugins/generative-ui/ui/grid.d.ts +6 -0
  49. package/dist/plugins/generative-ui/ui/index.d.ts +40 -0
  50. package/dist/plugins/generative-ui/ui/input-wrapper.d.ts +11 -0
  51. package/dist/plugins/generative-ui/ui/input.d.ts +3 -0
  52. package/dist/plugins/generative-ui/ui/label.d.ts +4 -0
  53. package/dist/plugins/generative-ui/ui/list.d.ts +6 -0
  54. package/dist/plugins/generative-ui/ui/metric.d.ts +7 -0
  55. package/dist/plugins/generative-ui/ui/pagination.d.ts +13 -0
  56. package/dist/plugins/generative-ui/ui/popover.d.ts +10 -0
  57. package/dist/plugins/generative-ui/ui/progress.d.ts +10 -0
  58. package/dist/plugins/generative-ui/ui/radio-group.d.ts +5 -0
  59. package/dist/plugins/generative-ui/ui/select-wrapper.d.ts +13 -0
  60. package/dist/plugins/generative-ui/ui/select.d.ts +15 -0
  61. package/dist/plugins/generative-ui/ui/separator.d.ts +4 -0
  62. package/dist/plugins/generative-ui/ui/skeleton-wrapper.d.ts +9 -0
  63. package/dist/plugins/generative-ui/ui/skeleton.d.ts +2 -0
  64. package/dist/plugins/generative-ui/ui/stack.d.ts +8 -0
  65. package/dist/plugins/generative-ui/ui/switch.d.ts +6 -0
  66. package/dist/plugins/generative-ui/ui/table.d.ts +10 -0
  67. package/dist/plugins/generative-ui/ui/tabs-wrapper.d.ts +21 -0
  68. package/dist/plugins/generative-ui/ui/tabs.d.ts +11 -0
  69. package/dist/plugins/generative-ui/ui/text.d.ts +7 -0
  70. package/dist/plugins/generative-ui/ui/textarea.d.ts +3 -0
  71. package/dist/plugins/generative-ui/ui/tooltip.d.ts +7 -0
  72. package/dist/plugins.cjs +1 -1
  73. package/dist/plugins.js +1 -1
  74. package/dist/{profiler-BaG0scxd.js → profiler-D4Tw5ecI.js} +2 -2
  75. package/dist/{profiler-BaG0scxd.js.map → profiler-D4Tw5ecI.js.map} +1 -1
  76. package/dist/{profiler-CuqENACf.cjs → profiler-DCWYDZ1F.cjs} +2 -2
  77. package/dist/{profiler-CuqENACf.cjs.map → profiler-DCWYDZ1F.cjs.map} +1 -1
  78. package/dist/{startRecording-BiLmoqZa.cjs → startRecording-3sTskM3H.cjs} +2 -2
  79. package/dist/{startRecording-BiLmoqZa.cjs.map → startRecording-3sTskM3H.cjs.map} +1 -1
  80. package/dist/{startRecording-86bHmd-l.js → startRecording-BHhcCWQE.js} +2 -2
  81. package/dist/{startRecording-86bHmd-l.js.map → startRecording-BHhcCWQE.js.map} +1 -1
  82. package/package.json +4 -1
  83. package/src/components/Chat/stories/Charts.stories.tsx +260 -0
  84. package/src/components/Chat/stories/ConnectionConfiguration.stories.tsx +6 -6
  85. package/src/components/Chat/stories/GenerativeUI.stories.tsx +113 -0
  86. package/src/components/Replay.stories.tsx +1 -1
  87. package/src/components/Replay.tsx +18 -13
  88. package/src/components/ui/charts.stories.tsx +246 -0
  89. package/src/components/ui/generative-ui.stories.tsx +557 -0
  90. package/src/components/ui/generative-ui.tsx +60 -360
  91. package/src/components/ui/tool-ui.stories.tsx +6 -3
  92. package/src/hooks/useAuth.ts +17 -1
  93. package/src/hooks/useFollowOnSuggestions.ts +6 -1
  94. package/src/plugins/chart/catalog.ts +141 -0
  95. package/src/plugins/chart/component.tsx +79 -125
  96. package/src/plugins/chart/index.ts +141 -89
  97. package/src/plugins/chart/ui/area-chart.tsx +133 -0
  98. package/src/plugins/chart/ui/bar-chart.tsx +137 -0
  99. package/src/plugins/chart/ui/donut-chart.tsx +167 -0
  100. package/src/plugins/chart/ui/index.ts +7 -0
  101. package/src/plugins/chart/ui/line-chart.tsx +135 -0
  102. package/src/plugins/chart/ui/pie-chart.tsx +148 -0
  103. package/src/plugins/chart/ui/radar-chart.tsx +105 -0
  104. package/src/plugins/chart/ui/scatter-chart.tsx +132 -0
  105. package/src/plugins/components/MacOSWindowFrame.tsx +55 -0
  106. package/src/plugins/components/PluginLoadingState.tsx +9 -13
  107. package/src/plugins/generative-ui/catalog.ts +277 -0
  108. package/src/plugins/generative-ui/component.tsx +112 -21
  109. package/src/plugins/generative-ui/index.ts +20 -141
  110. package/src/plugins/generative-ui/ui/accordion-wrapper.tsx +57 -0
  111. package/src/plugins/generative-ui/ui/accordion.tsx +66 -0
  112. package/src/plugins/generative-ui/ui/action-button.tsx +68 -0
  113. package/src/plugins/generative-ui/ui/alert-wrapper.tsx +26 -0
  114. package/src/plugins/generative-ui/ui/alert.tsx +66 -0
  115. package/src/plugins/generative-ui/ui/avatar-wrapper.tsx +22 -0
  116. package/src/plugins/generative-ui/ui/avatar.tsx +109 -0
  117. package/src/plugins/generative-ui/ui/badge.tsx +65 -0
  118. package/src/plugins/generative-ui/ui/button-wrapper.tsx +32 -0
  119. package/src/plugins/generative-ui/ui/button.tsx +65 -0
  120. package/src/plugins/generative-ui/ui/card-wrapper.tsx +36 -0
  121. package/src/plugins/generative-ui/ui/card.tsx +92 -0
  122. package/src/plugins/generative-ui/ui/checkbox-wrapper.tsx +39 -0
  123. package/src/plugins/generative-ui/ui/checkbox.tsx +32 -0
  124. package/src/plugins/generative-ui/ui/data-table.tsx +53 -0
  125. package/src/plugins/generative-ui/ui/dialog.tsx +158 -0
  126. package/src/plugins/generative-ui/ui/dropdown-menu.tsx +257 -0
  127. package/src/plugins/generative-ui/ui/grid.tsx +29 -0
  128. package/src/plugins/generative-ui/ui/index.ts +43 -0
  129. package/src/plugins/generative-ui/ui/input-wrapper.tsx +38 -0
  130. package/src/plugins/generative-ui/ui/input.tsx +21 -0
  131. package/src/plugins/generative-ui/ui/label.tsx +24 -0
  132. package/src/plugins/generative-ui/ui/list.tsx +34 -0
  133. package/src/plugins/generative-ui/ui/metric.tsx +53 -0
  134. package/src/plugins/generative-ui/ui/pagination.tsx +127 -0
  135. package/src/plugins/generative-ui/ui/popover.tsx +89 -0
  136. package/src/plugins/generative-ui/ui/progress.tsx +57 -0
  137. package/src/plugins/generative-ui/ui/radio-group.tsx +45 -0
  138. package/src/plugins/generative-ui/ui/select-wrapper.tsx +41 -0
  139. package/src/plugins/generative-ui/ui/select.tsx +190 -0
  140. package/src/plugins/generative-ui/ui/separator.tsx +28 -0
  141. package/src/plugins/generative-ui/ui/skeleton-wrapper.tsx +30 -0
  142. package/src/plugins/generative-ui/ui/skeleton.tsx +13 -0
  143. package/src/plugins/generative-ui/ui/stack.tsx +54 -0
  144. package/src/plugins/generative-ui/ui/switch.tsx +35 -0
  145. package/src/plugins/generative-ui/ui/table.tsx +116 -0
  146. package/src/plugins/generative-ui/ui/tabs-wrapper.tsx +51 -0
  147. package/src/plugins/generative-ui/ui/tabs.tsx +92 -0
  148. package/src/plugins/generative-ui/ui/text.tsx +33 -0
  149. package/src/plugins/generative-ui/ui/textarea.tsx +18 -0
  150. package/src/plugins/generative-ui/ui/tooltip.tsx +57 -0
  151. package/dist/components/Chat/stories/Plugins.stories.d.ts +0 -12
  152. package/dist/index-B8nSCdu4.cjs +0 -251
  153. package/dist/index-B8nSCdu4.cjs.map +0 -1
  154. package/dist/index-CAtaLV1E.cjs +0 -187
  155. package/dist/index-CAtaLV1E.cjs.map +0 -1
  156. package/dist/index-CJrwma08.js +0 -27232
  157. package/dist/index-CJrwma08.js.map +0 -1
  158. package/dist/index-DLWQ91ow.js +0 -40049
  159. package/dist/index-DLWQ91ow.js.map +0 -1
  160. package/src/components/Chat/stories/Plugins.stories.tsx +0 -158
@@ -1,149 +1,103 @@
1
1
  'use client'
2
2
 
3
3
  import { useDensity } from '@/hooks/useDensity'
4
- import { useRadius } from '@/hooks/useRadius'
5
4
  import { cn } from '@/lib/utils'
5
+ import { isJsonRenderTree, type JsonRenderNode } from '@/lib/generative-ui'
6
6
  import { SyntaxHighlighterProps } from '@assistant-ui/react-markdown'
7
7
  import { AlertCircleIcon } from 'lucide-react'
8
- import { FC, useEffect, useMemo, useRef, useState } from 'react'
9
- import { parse, View, Warn } from 'vega'
10
- import { expressionInterpreter } from 'vega-interpreter'
8
+ import { FC, useMemo } from 'react'
9
+ import { MacOSWindowFrame } from '../components/MacOSWindowFrame'
10
+ import { PluginLoadingState } from '../components/PluginLoadingState'
11
+
12
+ // Import all chart components
13
+ import {
14
+ BarChart,
15
+ LineChart,
16
+ AreaChart,
17
+ PieChart,
18
+ DonutChart,
19
+ ScatterChart,
20
+ RadarChart,
21
+ } from './ui'
22
+
23
+ const loadingMessages = [
24
+ 'Rendering chart...',
25
+ 'Visualizing data...',
26
+ 'Building chart...',
27
+ 'Processing data...',
28
+ ]
29
+
30
+ function getRandomLoadingMessage() {
31
+ return loadingMessages[Math.floor(Math.random() * loadingMessages.length)]
32
+ }
33
+
34
+ /**
35
+ * Chart components registry
36
+ */
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ const chartComponents: Record<string, FC<any>> = {
39
+ BarChart,
40
+ LineChart,
41
+ AreaChart,
42
+ PieChart,
43
+ DonutChart,
44
+ ScatterChart,
45
+ RadarChart,
46
+ }
47
+
48
+ /**
49
+ * Render a chart node from json-render tree
50
+ */
51
+ function renderChartNode(node: JsonRenderNode): React.ReactNode {
52
+ const Component = chartComponents[node.type]
53
+
54
+ if (!Component) {
55
+ return (
56
+ <div className="text-muted-foreground flex items-center gap-2 text-sm">
57
+ <AlertCircleIcon className="size-4" />
58
+ <span>Unknown chart type: {node.type}</span>
59
+ </div>
60
+ )
61
+ }
62
+
63
+ return <Component {...(node.props ?? {})} />
64
+ }
11
65
 
12
66
  export const ChartRenderer: FC<SyntaxHighlighterProps> = ({ code }) => {
13
- const containerRef = useRef<HTMLDivElement>(null)
14
- const viewRef = useRef<View | null>(null)
15
- const wrapperRef = useRef<HTMLDivElement>(null)
16
- const [error, setError] = useState<string | null>(null)
17
- const [chartReady, setChartReady] = useState(false)
18
- const [containerWidth, setContainerWidth] = useState(0)
19
- const r = useRadius()
20
67
  const d = useDensity()
21
68
 
22
- // Track container width so the Vega view can fill available space
23
- useEffect(() => {
24
- const el = wrapperRef.current
25
- if (!el) return
26
- const ro = new ResizeObserver(([entry]) => {
27
- setContainerWidth(entry.contentRect.width)
28
- })
29
- ro.observe(el)
30
- return () => ro.disconnect()
31
- }, [])
32
-
33
- // Parse and validate JSON in useMemo - only recomputes when code changes
34
- const parsedSpec = useMemo(() => {
69
+ // Parse JSON - returns null if invalid (still streaming)
70
+ const content = useMemo(() => {
35
71
  const trimmedCode = code.trim()
36
72
  if (!trimmedCode) return null
37
73
 
38
74
  try {
39
- const spec = JSON.parse(trimmedCode) as Record<string, unknown>
40
-
41
- // Validate that data array exists and has at least one record with values
42
- const dataArray = spec.data as Array<{ values?: unknown[] }> | undefined
43
- if (!dataArray?.length) return null
44
-
45
- const hasValidData = dataArray.some(
46
- (d) => Array.isArray(d.values) && d.values.length > 0
47
- )
48
- if (!hasValidData) return null
49
-
50
- return spec
75
+ const parsed = JSON.parse(trimmedCode)
76
+ // Validate it has a type field (basic json-render structure)
77
+ if (!isJsonRenderTree(parsed)) {
78
+ return null
79
+ }
80
+ return parsed
51
81
  } catch {
82
+ // JSON is incomplete (still streaming) - return null to show loading state
52
83
  return null
53
84
  }
54
85
  }, [code])
55
86
 
56
- // Only render when we have valid JSON
57
- const shouldRender = parsedSpec !== null
58
-
59
- // Build the spec with autosize and width derived from the container
60
- const sizedSpec = useMemo(() => {
61
- if (!parsedSpec || containerWidth === 0) return null
62
- // Padding used by the outer wrapper (p-lg ≈ 24px each side)
63
- const padding = 48
64
- const availableWidth = Math.max(containerWidth - padding, 100)
65
- return {
66
- ...parsedSpec,
67
- width: availableWidth,
68
- autosize: { type: 'fit' as const, contains: 'padding' as const },
69
- }
70
- }, [parsedSpec, containerWidth])
71
-
72
- useEffect(() => {
73
- if (!containerRef.current || !shouldRender || !sizedSpec) {
74
- return
75
- }
76
-
77
- setError(null)
78
- setChartReady(false)
79
-
80
- const runChart = async () => {
81
- try {
82
- // Clean up any existing view
83
- if (viewRef.current) {
84
- viewRef.current.finalize()
85
- viewRef.current = null
86
- }
87
-
88
- const chart = parse(sizedSpec, undefined, { ast: true })
89
- const view = new View(chart, {
90
- container: containerRef.current ?? undefined,
91
- renderer: 'svg',
92
- hover: true,
93
- logLevel: Warn,
94
- expr: expressionInterpreter,
95
- })
96
- viewRef.current = view
97
-
98
- await view.runAsync()
99
- setChartReady(true)
100
- } catch (err) {
101
- console.error('Failed to render chart:', err)
102
- setError(err instanceof Error ? err.message : 'Failed to render chart')
103
- }
104
- }
87
+ // Memoize the loading message so it doesn't change on every render
88
+ const loadingMessage = useMemo(() => getRandomLoadingMessage(), [])
105
89
 
106
- runChart()
107
-
108
- return () => {
109
- if (viewRef.current) {
110
- viewRef.current.finalize()
111
- viewRef.current = null
112
- }
113
- }
114
- }, [shouldRender, sizedSpec])
115
-
116
- const showLoading = !chartReady && !error
90
+ // Show loading shimmer while JSON is incomplete/streaming
91
+ if (!content) {
92
+ return <PluginLoadingState text={loadingMessage} />
93
+ }
117
94
 
95
+ // Render with macOS-style window frame
118
96
  return (
119
- <div
120
- ref={wrapperRef}
121
- className={cn(
122
- // the after:hidden is to prevent assistant-ui from showing its default code block loading indicator
123
- 'border-border relative min-h-[400px] w-full overflow-hidden border after:hidden',
124
- r('lg'),
125
- showLoading ? '' : d('p-lg')
126
- )}
127
- >
128
- {showLoading && (
129
- <div className="bg-muted absolute inset-0 z-10 flex items-center justify-center">
130
- <span className="shimmer text-muted-foreground text-sm">
131
- Rendering chart...
132
- </span>
133
- </div>
134
- )}
135
-
136
- {error && (
137
- <div className="bg-background absolute inset-0 z-10 flex items-center justify-center gap-2 text-rose-500">
138
- <AlertCircleIcon name="alert-circle" className="h-4 w-4" />
139
- {error}
140
- </div>
141
- )}
142
-
143
- <div
144
- ref={containerRef}
145
- className={error || showLoading ? 'invisible' : 'block'}
146
- />
147
- </div>
97
+ <MacOSWindowFrame>
98
+ <div className={cn('bg-card w-full', d('p-lg'))}>
99
+ {renderChartNode(content)}
100
+ </div>
101
+ </MacOSWindowFrame>
148
102
  )
149
103
  }
@@ -2,108 +2,160 @@ import { Plugin } from '@/types/plugins'
2
2
  import { ChartRenderer } from './component'
3
3
 
4
4
  /**
5
- * This plugin renders Vega charts.
5
+ * This plugin renders charts using json-render format.
6
6
  */
7
7
  export const chart: Plugin = {
8
- language: 'vega',
9
- prompt: `When a user requests a chart or visualization, respond with a valid Vega specification (https://vega.github.io/vega/) in a code block annotated with the language identifier 'vega'.
8
+ language: 'chart',
9
+ prompt: `WHEN TO USE CHARTS:
10
+ Create charts to visualize numerical data when it helps users understand patterns, trends, or comparisons. Use the 'chart' code block format.
10
11
 
11
12
  CRITICAL JSON REQUIREMENTS:
12
13
  - The code block MUST contain ONLY valid, parseable JSON
13
14
  - NO comments (no // or /* */ anywhere)
14
15
  - NO trailing commas
15
16
  - Use double quotes for all strings and keys
16
- - NO text before or after the JSON object
17
17
  - The JSON must start with { and end with }
18
18
 
19
+ AVAILABLE CHART TYPES:
20
+
21
+ BarChart - Compare categorical data
22
+ props: {
23
+ title?: string,
24
+ data: [{ label: string, value: number, color?: string }],
25
+ layout?: "vertical" | "horizontal",
26
+ showGrid?: boolean,
27
+ showLegend?: boolean
28
+ }
29
+
30
+ LineChart - Show trends over time
31
+ props: {
32
+ title?: string,
33
+ data: [{ label: string, [series]: number }],
34
+ series?: string[],
35
+ showGrid?: boolean,
36
+ showLegend?: boolean,
37
+ showDots?: boolean,
38
+ curved?: boolean
39
+ }
40
+
41
+ AreaChart - Show volume over time
42
+ props: {
43
+ title?: string,
44
+ data: [{ label: string, [series]: number }],
45
+ series?: string[],
46
+ stacked?: boolean,
47
+ showGrid?: boolean,
48
+ showLegend?: boolean
49
+ }
50
+
51
+ PieChart - Show proportions (2-6 categories)
52
+ props: {
53
+ title?: string,
54
+ data: [{ label: string, value: number, color?: string }],
55
+ showLabels?: boolean,
56
+ showLegend?: boolean
57
+ }
58
+
59
+ DonutChart - Proportions with center metric
60
+ props: {
61
+ title?: string,
62
+ data: [{ label: string, value: number, color?: string }],
63
+ showLabels?: boolean,
64
+ showLegend?: boolean,
65
+ innerLabel?: string,
66
+ innerValue?: string | number
67
+ }
68
+
69
+ ScatterChart - Show correlation
70
+ props: {
71
+ title?: string,
72
+ data: [{ x: number, y: number, label?: string, size?: number, color?: string }],
73
+ xLabel?: string,
74
+ yLabel?: string,
75
+ showGrid?: boolean
76
+ }
77
+
78
+ RadarChart - Compare multiple attributes (3-8 dimensions)
79
+ props: {
80
+ title?: string,
81
+ data: [{ label: string, value: number }],
82
+ showLegend?: boolean
83
+ }
84
+
85
+ STRUCTURE:
86
+ {
87
+ "type": "ChartType",
88
+ "props": { ... }
89
+ }
90
+
91
+ EXAMPLE - BAR CHART:
92
+ {
93
+ "type": "BarChart",
94
+ "props": {
95
+ "title": "Sales by Region",
96
+ "data": [
97
+ { "label": "North", "value": 120000 },
98
+ { "label": "South", "value": 98000 },
99
+ { "label": "East", "value": 145000 },
100
+ { "label": "West", "value": 87000 }
101
+ ]
102
+ }
103
+ }
104
+
105
+ EXAMPLE - LINE CHART (multiple series):
106
+ {
107
+ "type": "LineChart",
108
+ "props": {
109
+ "title": "Monthly Revenue",
110
+ "data": [
111
+ { "label": "Jan", "revenue": 45000, "costs": 32000 },
112
+ { "label": "Feb", "revenue": 52000, "costs": 35000 },
113
+ { "label": "Mar", "revenue": 61000, "costs": 38000 }
114
+ ],
115
+ "series": ["revenue", "costs"]
116
+ }
117
+ }
118
+
119
+ EXAMPLE - PIE CHART:
120
+ {
121
+ "type": "PieChart",
122
+ "props": {
123
+ "title": "Market Share",
124
+ "data": [
125
+ { "label": "Product A", "value": 45 },
126
+ { "label": "Product B", "value": 30 },
127
+ { "label": "Product C", "value": 25 }
128
+ ]
129
+ }
130
+ }
131
+
132
+ EXAMPLE - DONUT CHART:
133
+ {
134
+ "type": "DonutChart",
135
+ "props": {
136
+ "title": "Budget Allocation",
137
+ "data": [
138
+ { "label": "Marketing", "value": 35 },
139
+ { "label": "Engineering", "value": 45 },
140
+ { "label": "Operations", "value": 20 }
141
+ ],
142
+ "innerValue": "$1.2M",
143
+ "innerLabel": "Total Budget"
144
+ }
145
+ }
146
+
147
+ CHART SELECTION GUIDE:
148
+ - Comparing categories → BarChart
149
+ - Trends over time → LineChart
150
+ - Volume/magnitude over time → AreaChart
151
+ - Part-to-whole (2-6 items) → PieChart or DonutChart
152
+ - Correlation between variables → ScatterChart
153
+ - Multi-dimensional comparison → RadarChart
154
+
19
155
  CONTENT GUIDELINES:
20
156
  - Outside the code block, describe trends and insights found in the data
21
157
  - Do not describe visual properties or technical implementation details
22
- - Do not mention "Vega" or other technical terms - this is user-facing
23
-
24
- The Vega spec will be parsed with JSON.parse() - if it fails, the chart will error. Ensure strict JSON validity.
25
-
26
- REQUIRED STRUCTURE:
27
- Every spec needs: "$schema", "width", "height", "data", "scales", "marks". Include "padding" (5 or object) and "axes" for readability.
28
- Data format:
29
- {"name": "table", "values": [{"category": "A", "amount": 28}]}
30
- SCALES - Choose the right type:
31
- - "band": categorical x-axis (bar charts) - domain from data field, range: "width", padding: 0.1
32
- - "linear": numerical axes - domain from data field, range: "width"/"height", nice: true
33
- - "time"/"utc": temporal data
34
- - "ordinal": for colors use range: {"scheme": "category10"} or range: ["#1f77b4", "#ff7f0e", "#2ca02c"]
35
- MARKS - Common types:
36
- - "rect": bar charts (requires x, width, y, y2)
37
- - "line": time series (requires x, y)
38
- - "area": filled areas (requires x, y, y2)
39
- - "symbol": scatter plots (requires x, y)
40
- CHART PATTERNS:
41
- Bar: band scale (x) + linear scale (y) + rect marks. Set y2: {"scale": "yscale", "value": 0}
42
- Line: linear/point scale (x) + linear scale (y) + line mark. Add "interpolate": "monotone"
43
- Scatter: linear scales (both) + symbol marks
44
- Area: like line but use area mark with y2: {"scale": "yscale", "value": 0}
45
- Stacked: add transform [{"type": "stack", "groupby": ["x"], "field": "y"}], use y0/y1 fields
46
- CRITICAL RULES:
47
- 1. Data must contain at least one record with valid (non-null) values for ALL fields used in scales
48
- 2. ONLY reference fields that actually exist in your data - never use datum.meta, datum.id, or any field not in your values array
49
- 3. Always include y2 for rect/area marks (or bars/areas have zero height)
50
- 4. Use "band" for categories, not "linear"
51
- 5. For position scales use "range": "width" or "height". For color scales NEVER use "range": "category10" - use "range": {"scheme": "category10"} or an array
52
- 6. Match scale/data names exactly between definition and usage
53
- 7. Include "from": {"data": "dataName"} on marks
54
- 8. Add padding to prevent label cutoff
55
- EXAMPLE: COMPLETE BAR CHART
56
- {
57
- "$schema": "https://vega.github.io/schema/vega/v5.json",
58
- "width": 500,
59
- "height": 300,
60
- "padding": 5,
61
- "data": [
62
- {
63
- "name": "table",
64
- "values": [
65
- {"category": "A", "amount": 28},
66
- {"category": "B", "amount": 55},
67
- {"category": "C", "amount": 43}
68
- ]
69
- }
70
- ],
71
- "scales": [
72
- {
73
- "name": "xscale",
74
- "type": "band",
75
- "domain": {"data": "table", "field": "category"},
76
- "range": "width",
77
- "padding": 0.1
78
- },
79
- {
80
- "name": "yscale",
81
- "type": "linear",
82
- "domain": {"data": "table", "field": "amount"},
83
- "range": "height",
84
- "nice": true
85
- }
86
- ],
87
- "axes": [
88
- {"scale": "xscale", "orient": "bottom"},
89
- {"scale": "yscale", "orient": "left", "title": "Amount"}
90
- ],
91
- "marks": [
92
- {
93
- "type": "rect",
94
- "from": {"data": "table"},
95
- "encode": {
96
- "enter": {
97
- "x": {"scale": "xscale", "field": "category"},
98
- "width": {"scale": "xscale", "band": 1},
99
- "y": {"scale": "yscale", "field": "amount"},
100
- "y2": {"scale": "yscale", "value": 0},
101
- "fill": {"value": "steelblue"}
102
- }
103
- }
104
- }
105
- ]
106
- }`,
158
+ - Focus on what the data means, not how it's displayed`,
107
159
  Component: ChartRenderer,
108
160
  Header: undefined,
109
161
  }
@@ -0,0 +1,133 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@/lib/utils'
4
+ import { FC, useMemo } from 'react'
5
+ import {
6
+ AreaChart as RechartsAreaChart,
7
+ Area,
8
+ XAxis,
9
+ YAxis,
10
+ CartesianGrid,
11
+ Tooltip,
12
+ Legend,
13
+ ResponsiveContainer,
14
+ TooltipProps,
15
+ } from 'recharts'
16
+
17
+ const CustomTooltip = ({ active, payload }: TooltipProps<number, string>) => {
18
+ if (!active || !payload || payload.length === 0) return null
19
+ return (
20
+ <div className="bg-background text-foreground border-border rounded-md border px-2 py-1.5 text-xs shadow-sm">
21
+ {payload.map((entry, index) => (
22
+ <div key={index} className="flex items-center gap-2">
23
+ <span
24
+ className="h-2 w-2 rounded-full"
25
+ style={{ backgroundColor: entry.color }}
26
+ />
27
+ <span>{entry.name}:</span>
28
+ <span className="font-medium">
29
+ {typeof entry.value === 'number'
30
+ ? entry.value.toLocaleString()
31
+ : entry.value}
32
+ </span>
33
+ </div>
34
+ ))}
35
+ </div>
36
+ )
37
+ }
38
+
39
+ const COLORS = [
40
+ 'var(--chart-1)',
41
+ 'var(--chart-2)',
42
+ 'var(--chart-3)',
43
+ 'var(--chart-4)',
44
+ 'var(--chart-5)',
45
+ ]
46
+
47
+ interface SeriesDataPoint {
48
+ label: string
49
+ [key: string]: string | number
50
+ }
51
+
52
+ export interface AreaChartProps {
53
+ title?: string
54
+ data: SeriesDataPoint[]
55
+ series?: string[]
56
+ stacked?: boolean
57
+ showGrid?: boolean
58
+ showLegend?: boolean
59
+ className?: string
60
+ }
61
+
62
+ export const AreaChart: FC<AreaChartProps> = ({
63
+ title,
64
+ data,
65
+ series,
66
+ stacked = false,
67
+ showGrid = true,
68
+ showLegend = true,
69
+ className,
70
+ }) => {
71
+ // Auto-detect series from data keys if not provided
72
+ const seriesKeys = useMemo(() => {
73
+ if (series && series.length > 0) return series
74
+ if (data.length === 0) return []
75
+ const keys = Object.keys(data[0]).filter((k) => k !== 'label')
76
+ return keys
77
+ }, [data, series])
78
+
79
+ return (
80
+ <div className={cn('flex flex-col gap-2', className)}>
81
+ {title && (
82
+ <h3 className="text-foreground text-sm font-medium">{title}</h3>
83
+ )}
84
+ <div className="h-[250px] w-full">
85
+ <ResponsiveContainer width="100%" height="100%">
86
+ <RechartsAreaChart
87
+ data={data}
88
+ margin={{ top: 10, right: 10, left: 10, bottom: 10 }}
89
+ >
90
+ {showGrid && (
91
+ <CartesianGrid
92
+ strokeDasharray="3 3"
93
+ className="stroke-muted/30"
94
+ />
95
+ )}
96
+ <XAxis
97
+ dataKey="label"
98
+ tick={{ fill: 'var(--foreground)', fontSize: 12 }}
99
+ axisLine={{ stroke: 'var(--border)' }}
100
+ tickLine={{ stroke: 'var(--border)' }}
101
+ />
102
+ <YAxis
103
+ tick={{ fill: 'var(--foreground)', fontSize: 12 }}
104
+ axisLine={{ stroke: 'var(--border)' }}
105
+ tickLine={{ stroke: 'var(--border)' }}
106
+ />
107
+ <Tooltip content={<CustomTooltip />} />
108
+ {showLegend && seriesKeys.length > 1 && (
109
+ <Legend
110
+ wrapperStyle={{ color: 'var(--foreground)' }}
111
+ formatter={(value) => (
112
+ <span style={{ color: 'var(--foreground)' }}>{value}</span>
113
+ )}
114
+ />
115
+ )}
116
+ {seriesKeys.map((key, index) => (
117
+ <Area
118
+ key={key}
119
+ type="monotone"
120
+ dataKey={key}
121
+ stackId={stacked ? 'stack' : undefined}
122
+ stroke={COLORS[index % COLORS.length]}
123
+ fill={COLORS[index % COLORS.length]}
124
+ fillOpacity={0.3}
125
+ isAnimationActive={false}
126
+ />
127
+ ))}
128
+ </RechartsAreaChart>
129
+ </ResponsiveContainer>
130
+ </div>
131
+ </div>
132
+ )
133
+ }