@gram-ai/elements 1.33.2 → 1.35.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 (243) hide show
  1. package/dist/compat-shims-CO9JXXV4.cjs.map +1 -1
  2. package/dist/compat-shims-DxtUrORi.js.map +1 -1
  3. package/dist/compat-shims.d.ts +9 -8
  4. package/dist/components/Chat/index.d.ts +2 -1
  5. package/dist/components/ChatHistory.d.ts +1 -1
  6. package/dist/components/FrontendTools/index.d.ts +1 -1
  7. package/dist/components/Replay.d.ts +1 -1
  8. package/dist/components/Replay.stories.d.ts +2 -2
  9. package/dist/components/ShadowRoot.d.ts +1 -1
  10. package/dist/components/ShareButton/index.d.ts +1 -1
  11. package/dist/components/ui/avatar.d.ts +3 -3
  12. package/dist/components/ui/button.d.ts +2 -2
  13. package/dist/components/ui/buttonVariants.d.ts +2 -2
  14. package/dist/components/ui/calendar.d.ts +2 -1
  15. package/dist/components/ui/collapsible.d.ts +3 -3
  16. package/dist/components/ui/dialog.d.ts +10 -10
  17. package/dist/components/ui/popover.d.ts +4 -4
  18. package/dist/components/ui/skeleton.d.ts +1 -1
  19. package/dist/components/ui/time-range-picker.d.ts +18 -1
  20. package/dist/components/ui/time-range-picker.test.d.ts +1 -0
  21. package/dist/components/ui/tool-ui.d.ts +7 -7
  22. package/dist/components/ui/tooltip.d.ts +4 -4
  23. package/dist/contexts/ConnectionStatusContext.d.ts +1 -1
  24. package/dist/contexts/ElementsProvider.d.ts +1 -1
  25. package/dist/contexts/ToolApprovalContext.d.ts +2 -2
  26. package/dist/contexts/ToolExecutionContext.d.ts +1 -1
  27. package/dist/contexts/portal-container.d.ts +1 -1
  28. package/dist/elements.cjs +1 -1
  29. package/dist/elements.css +1 -1
  30. package/dist/elements.js +19 -16
  31. package/dist/hooks/useDensity.d.ts +1 -1
  32. package/dist/hooks/useElements.d.ts +2 -1
  33. package/dist/hooks/useGramThreadListAdapter.d.ts +26 -0
  34. package/dist/hooks/useRadius.d.ts +1 -1
  35. package/dist/hooks/useThemeProps.d.ts +1 -1
  36. package/dist/hooks/useToolApproval.d.ts +2 -1
  37. package/dist/{index-reVrRxP1.js → index-BhIowiZF.js} +9744 -9493
  38. package/dist/index-BhIowiZF.js.map +1 -0
  39. package/dist/{index-DAWGW1Nj.cjs → index-D0jIGQr7.cjs} +43 -43
  40. package/dist/index-D0jIGQr7.cjs.map +1 -0
  41. package/dist/{index-CGoLfO5p.js → index-Dz13dSDa.js} +108 -51
  42. package/dist/index-Dz13dSDa.js.map +1 -0
  43. package/dist/index-PXd3rs95.cjs +194 -0
  44. package/dist/index-PXd3rs95.cjs.map +1 -0
  45. package/dist/index.d.ts +4 -1
  46. package/dist/lib/errorTracking.d.ts +1 -1
  47. package/dist/lib/messageConverter.d.ts +58 -8
  48. package/dist/lib/models.d.ts +1 -1
  49. package/dist/lib/tools.d.ts +11 -10
  50. package/dist/lib/utils.d.ts +2 -0
  51. package/dist/plugins/generative-ui/catalog.d.ts +3 -3
  52. package/dist/plugins/generative-ui/ui/accordion-wrapper.d.ts +2 -2
  53. package/dist/plugins/generative-ui/ui/accordion.d.ts +4 -4
  54. package/dist/plugins/generative-ui/ui/action-button.d.ts +1 -1
  55. package/dist/plugins/generative-ui/ui/alert-wrapper.d.ts +2 -1
  56. package/dist/plugins/generative-ui/ui/alert.d.ts +3 -3
  57. package/dist/plugins/generative-ui/ui/avatar-wrapper.d.ts +2 -1
  58. package/dist/plugins/generative-ui/ui/avatar.d.ts +6 -6
  59. package/dist/plugins/generative-ui/ui/badge.d.ts +2 -2
  60. package/dist/plugins/generative-ui/ui/button-wrapper.d.ts +2 -1
  61. package/dist/plugins/generative-ui/ui/button.d.ts +3 -3
  62. package/dist/plugins/generative-ui/ui/card-wrapper.d.ts +1 -1
  63. package/dist/plugins/generative-ui/ui/card.d.ts +7 -7
  64. package/dist/plugins/generative-ui/ui/checkbox-wrapper.d.ts +2 -1
  65. package/dist/plugins/generative-ui/ui/checkbox.d.ts +1 -1
  66. package/dist/plugins/generative-ui/ui/data-table.d.ts +1 -1
  67. package/dist/plugins/generative-ui/ui/dialog.d.ts +10 -10
  68. package/dist/plugins/generative-ui/ui/dropdown-menu.d.ts +15 -15
  69. package/dist/plugins/generative-ui/ui/grid.d.ts +1 -1
  70. package/dist/plugins/generative-ui/ui/index.d.ts +57 -40
  71. package/dist/plugins/generative-ui/ui/input-wrapper.d.ts +2 -1
  72. package/dist/plugins/generative-ui/ui/input.d.ts +1 -1
  73. package/dist/plugins/generative-ui/ui/label.d.ts +1 -1
  74. package/dist/plugins/generative-ui/ui/list.d.ts +2 -1
  75. package/dist/plugins/generative-ui/ui/metric.d.ts +1 -1
  76. package/dist/plugins/generative-ui/ui/pagination.d.ts +7 -7
  77. package/dist/plugins/generative-ui/ui/popover.d.ts +7 -7
  78. package/dist/plugins/generative-ui/ui/progress.d.ts +1 -1
  79. package/dist/plugins/generative-ui/ui/radio-group.d.ts +2 -2
  80. package/dist/plugins/generative-ui/ui/select-wrapper.d.ts +2 -1
  81. package/dist/plugins/generative-ui/ui/select.d.ts +10 -10
  82. package/dist/plugins/generative-ui/ui/separator.d.ts +1 -1
  83. package/dist/plugins/generative-ui/ui/skeleton-wrapper.d.ts +2 -1
  84. package/dist/plugins/generative-ui/ui/skeleton.d.ts +1 -1
  85. package/dist/plugins/generative-ui/ui/stack.d.ts +1 -1
  86. package/dist/plugins/generative-ui/ui/switch.d.ts +1 -1
  87. package/dist/plugins/generative-ui/ui/table.d.ts +8 -8
  88. package/dist/plugins/generative-ui/ui/tabs-wrapper.d.ts +2 -2
  89. package/dist/plugins/generative-ui/ui/tabs.d.ts +4 -4
  90. package/dist/plugins/generative-ui/ui/text.d.ts +1 -1
  91. package/dist/plugins/generative-ui/ui/textarea.d.ts +1 -1
  92. package/dist/plugins/generative-ui/ui/tooltip.d.ts +4 -4
  93. package/dist/plugins.cjs +1 -1
  94. package/dist/plugins.js +1 -1
  95. package/dist/{profiler-noho3NG9.js → profiler-CtGKTWWP.js} +2 -2
  96. package/dist/{profiler-noho3NG9.js.map → profiler-CtGKTWWP.js.map} +1 -1
  97. package/dist/{profiler-B3tfiOx4.cjs → profiler-l7_HjTyw.cjs} +2 -2
  98. package/dist/{profiler-B3tfiOx4.cjs.map → profiler-l7_HjTyw.cjs.map} +1 -1
  99. package/dist/react-shim.cjs.map +1 -1
  100. package/dist/react-shim.d.ts +1 -1
  101. package/dist/react-shim.js +1 -4
  102. package/dist/react-shim.js.map +1 -1
  103. package/dist/server/bun.cjs.map +1 -1
  104. package/dist/server/bun.js.map +1 -1
  105. package/dist/server/express.cjs.map +1 -1
  106. package/dist/server/express.js.map +1 -1
  107. package/dist/server/fastify.cjs.map +1 -1
  108. package/dist/server/fastify.js.map +1 -1
  109. package/dist/server/hono.cjs.map +1 -1
  110. package/dist/server/hono.js.map +1 -1
  111. package/dist/server/nextjs.cjs.map +1 -1
  112. package/dist/server/nextjs.js.map +1 -1
  113. package/dist/server/tanstack-start.cjs.map +1 -1
  114. package/dist/server/tanstack-start.js.map +1 -1
  115. package/dist/{startRecording-7Oy6wM18.cjs → startRecording-DEw2Aeq4.cjs} +2 -2
  116. package/dist/{startRecording-7Oy6wM18.cjs.map → startRecording-DEw2Aeq4.cjs.map} +1 -1
  117. package/dist/{startRecording-mkmig-2n.js → startRecording-iYEL0-vr.js} +2 -2
  118. package/dist/{startRecording-mkmig-2n.js.map → startRecording-iYEL0-vr.js.map} +1 -1
  119. package/dist/types/index.d.ts +93 -4
  120. package/package.json +8 -9
  121. package/src/compat-shims.ts +16 -2
  122. package/src/components/Chat/index.tsx +4 -1
  123. package/src/components/Chat/stories/FrontendTools.stories.tsx +1 -1
  124. package/src/components/Chat/stories/ToolApproval.stories.tsx +2 -2
  125. package/src/components/Chat/stories/Tools.stories.tsx +13 -5
  126. package/src/components/ChatHistory.tsx +3 -1
  127. package/src/components/FrontendTools/index.tsx +1 -1
  128. package/src/components/MessageContent.tsx +1 -0
  129. package/src/components/Replay.stories.tsx +2 -3
  130. package/src/components/Replay.tsx +17 -10
  131. package/src/components/ShadowRoot.tsx +2 -2
  132. package/src/components/ShareButton/index.tsx +4 -2
  133. package/src/components/assistant-ui/assistant-modal.tsx +5 -3
  134. package/src/components/assistant-ui/attachment.tsx +1 -1
  135. package/src/components/assistant-ui/error-boundary.tsx +1 -1
  136. package/src/components/assistant-ui/markdown-text.tsx +1 -1
  137. package/src/components/assistant-ui/thread.tsx +256 -14
  138. package/src/components/assistant-ui/tool-mention-autocomplete.tsx +1 -1
  139. package/src/components/ui/avatar.tsx +3 -3
  140. package/src/components/ui/calendar.tsx +1 -1
  141. package/src/components/ui/collapsible.tsx +7 -3
  142. package/src/components/ui/dialog.tsx +18 -10
  143. package/src/components/ui/generative-ui.tsx +9 -4
  144. package/src/components/ui/popover.tsx +4 -4
  145. package/src/components/ui/skeleton.tsx +4 -1
  146. package/src/components/ui/time-range-picker.stories.tsx +164 -154
  147. package/src/components/ui/time-range-picker.test.ts +57 -0
  148. package/src/components/ui/time-range-picker.tsx +40 -9
  149. package/src/components/ui/tool-ui.tsx +18 -9
  150. package/src/components/ui/tooltip.tsx +4 -4
  151. package/src/contexts/ChatIdContext.tsx +1 -1
  152. package/src/contexts/ConnectionStatusContext.tsx +6 -5
  153. package/src/contexts/ElementsProvider.tsx +109 -37
  154. package/src/contexts/ReplayContext.ts +1 -1
  155. package/src/contexts/ToolApprovalContext.tsx +5 -1
  156. package/src/contexts/ToolExecutionContext.tsx +1 -1
  157. package/src/contexts/portal-container.tsx +1 -1
  158. package/src/hooks/useAuth.ts +2 -1
  159. package/src/hooks/useDensity.ts +1 -1
  160. package/src/hooks/useElements.ts +2 -1
  161. package/src/hooks/useFollowOnSuggestions.ts +3 -6
  162. package/src/hooks/useGramThreadListAdapter.tsx +118 -9
  163. package/src/hooks/useMCPTools.ts +2 -2
  164. package/src/hooks/useModel.ts +1 -3
  165. package/src/hooks/usePluginComponents.ts +3 -1
  166. package/src/hooks/useRadius.ts +1 -1
  167. package/src/hooks/useSession.ts +3 -1
  168. package/src/hooks/useThemeProps.ts +5 -5
  169. package/src/hooks/useToolApproval.ts +2 -1
  170. package/src/index.ts +16 -0
  171. package/src/lib/cassette.ts +21 -27
  172. package/src/lib/contextCompaction.test.ts +2 -2
  173. package/src/lib/contextCompaction.ts +20 -8
  174. package/src/lib/errorTracking.ts +1 -4
  175. package/src/lib/messageConverter.test.ts +11 -13
  176. package/src/lib/messageConverter.ts +105 -58
  177. package/src/lib/models.ts +19 -7
  178. package/src/lib/token.ts +2 -5
  179. package/src/lib/tool-mentions.ts +5 -2
  180. package/src/lib/tools.byte-cap.test.ts +1 -1
  181. package/src/lib/tools.test.ts +1 -1
  182. package/src/lib/tools.ts +15 -5
  183. package/src/lib/utils.ts +22 -2
  184. package/src/lib.d.ts +8 -1
  185. package/src/plugins/chart/chart.test.ts +3 -4
  186. package/src/plugins/chart/component.tsx +7 -6
  187. package/src/plugins/chart/ui/area-chart.tsx +1 -1
  188. package/src/plugins/chart/ui/line-chart.tsx +1 -1
  189. package/src/plugins/generative-ui/ui/accordion-wrapper.tsx +2 -2
  190. package/src/plugins/generative-ui/ui/accordion.tsx +4 -4
  191. package/src/plugins/generative-ui/ui/action-button.tsx +4 -2
  192. package/src/plugins/generative-ui/ui/alert-wrapper.tsx +1 -1
  193. package/src/plugins/generative-ui/ui/alert.tsx +7 -3
  194. package/src/plugins/generative-ui/ui/avatar-wrapper.tsx +5 -1
  195. package/src/plugins/generative-ui/ui/avatar.tsx +12 -6
  196. package/src/plugins/generative-ui/ui/badge.tsx +1 -1
  197. package/src/plugins/generative-ui/ui/button-wrapper.tsx +1 -1
  198. package/src/plugins/generative-ui/ui/button.tsx +1 -1
  199. package/src/plugins/generative-ui/ui/card-wrapper.tsx +1 -1
  200. package/src/plugins/generative-ui/ui/card.tsx +28 -7
  201. package/src/plugins/generative-ui/ui/checkbox-wrapper.tsx +1 -1
  202. package/src/plugins/generative-ui/ui/checkbox.tsx +1 -1
  203. package/src/plugins/generative-ui/ui/data-table.tsx +1 -1
  204. package/src/plugins/generative-ui/ui/dialog.tsx +15 -10
  205. package/src/plugins/generative-ui/ui/dropdown-menu.tsx +33 -15
  206. package/src/plugins/generative-ui/ui/grid.tsx +1 -1
  207. package/src/plugins/generative-ui/ui/index.ts +154 -40
  208. package/src/plugins/generative-ui/ui/input-wrapper.tsx +1 -1
  209. package/src/plugins/generative-ui/ui/input.tsx +5 -1
  210. package/src/plugins/generative-ui/ui/label.tsx +1 -1
  211. package/src/plugins/generative-ui/ui/list.tsx +5 -1
  212. package/src/plugins/generative-ui/ui/metric.tsx +2 -1
  213. package/src/plugins/generative-ui/ui/pagination.tsx +12 -7
  214. package/src/plugins/generative-ui/ui/popover.tsx +13 -7
  215. package/src/plugins/generative-ui/ui/progress.tsx +1 -1
  216. package/src/plugins/generative-ui/ui/radio-group.tsx +2 -2
  217. package/src/plugins/generative-ui/ui/select-wrapper.tsx +1 -1
  218. package/src/plugins/generative-ui/ui/select.tsx +14 -10
  219. package/src/plugins/generative-ui/ui/separator.tsx +1 -1
  220. package/src/plugins/generative-ui/ui/skeleton-wrapper.tsx +1 -1
  221. package/src/plugins/generative-ui/ui/skeleton.tsx +4 -1
  222. package/src/plugins/generative-ui/ui/stack.tsx +1 -1
  223. package/src/plugins/generative-ui/ui/switch.tsx +1 -1
  224. package/src/plugins/generative-ui/ui/table.tsx +29 -8
  225. package/src/plugins/generative-ui/ui/tabs-wrapper.tsx +5 -2
  226. package/src/plugins/generative-ui/ui/tabs.tsx +4 -4
  227. package/src/plugins/generative-ui/ui/text.tsx +1 -1
  228. package/src/plugins/generative-ui/ui/textarea.tsx +4 -1
  229. package/src/plugins/generative-ui/ui/tooltip.tsx +4 -4
  230. package/src/react-shim.ts +9 -4
  231. package/src/server/bun.ts +1 -1
  232. package/src/server/express.ts +1 -1
  233. package/src/server/fastify.ts +1 -1
  234. package/src/server/hono.ts +1 -1
  235. package/src/server/nextjs.ts +1 -1
  236. package/src/server/tanstack-start.ts +1 -1
  237. package/src/storybook.d.ts +5 -0
  238. package/src/types/index.ts +112 -4
  239. package/dist/index-BCV7Zf9E.cjs +0 -194
  240. package/dist/index-BCV7Zf9E.cjs.map +0 -1
  241. package/dist/index-CGoLfO5p.js.map +0 -1
  242. package/dist/index-DAWGW1Nj.cjs.map +0 -1
  243. package/dist/index-reVrRxP1.js.map +0 -1
@@ -36,171 +36,214 @@ const meta: Meta<typeof TimeRangePicker> = {
36
36
  export default meta;
37
37
  type Story = StoryObj<typeof TimeRangePicker>;
38
38
 
39
+ function DefaultStory() {
40
+ const [preset, setPreset] = useState<DateRangePreset | null>("7d");
41
+ const [customRange, setCustomRange] = useState<TimeRange | null>(null);
42
+
43
+ return (
44
+ <TimeRangePicker
45
+ preset={customRange ? null : preset}
46
+ customRange={customRange}
47
+ onPresetChange={(p) => {
48
+ setPreset(p);
49
+ setCustomRange(null);
50
+ }}
51
+ onCustomRangeChange={(from, to, label) => {
52
+ setCustomRange({ from, to });
53
+ setPreset(null);
54
+ console.log("Custom range:", { from, to, label });
55
+ }}
56
+ onClearCustomRange={() => {
57
+ setCustomRange(null);
58
+ setPreset("7d");
59
+ }}
60
+ />
61
+ );
62
+ }
63
+
39
64
  /**
40
65
  * Default time range picker with preset badges and calendar.
41
66
  * Supports natural language input with AI parsing.
42
67
  */
43
68
  export const Default: Story = {
44
- render: () => {
45
- const [preset, setPreset] = useState<DateRangePreset | null>("7d");
46
- const [customRange, setCustomRange] = useState<TimeRange | null>(null);
47
-
48
- return (
49
- <TimeRangePicker
50
- preset={customRange ? null : preset}
51
- customRange={customRange}
52
- onPresetChange={(p) => {
53
- setPreset(p);
54
- setCustomRange(null);
55
- }}
56
- onCustomRangeChange={(from, to, label) => {
57
- setCustomRange({ from, to });
58
- setPreset(null);
59
- console.log("Custom range:", { from, to, label });
60
- }}
61
- onClearCustomRange={() => {
62
- setCustomRange(null);
63
- setPreset("7d");
64
- }}
65
- />
66
- );
67
- },
69
+ render: () => <DefaultStory />,
68
70
  };
69
71
 
72
+ function WithTimezoneStory() {
73
+ const [preset, setPreset] = useState<DateRangePreset | null>("30d");
74
+ const [customRange, setCustomRange] = useState<TimeRange | null>(null);
75
+
76
+ return (
77
+ <TimeRangePicker
78
+ preset={customRange ? null : preset}
79
+ customRange={customRange}
80
+ onPresetChange={(p) => {
81
+ setPreset(p);
82
+ setCustomRange(null);
83
+ }}
84
+ onCustomRangeChange={(from, to) => {
85
+ setCustomRange({ from, to });
86
+ setPreset(null);
87
+ }}
88
+ onClearCustomRange={() => {
89
+ setCustomRange(null);
90
+ setPreset("30d");
91
+ }}
92
+ timezone="UTC-08:00"
93
+ />
94
+ );
95
+ }
96
+
70
97
  /**
71
98
  * Time range picker with timezone indicator.
72
99
  */
73
100
  export const WithTimezone: Story = {
74
- render: () => {
75
- const [preset, setPreset] = useState<DateRangePreset | null>("30d");
76
- const [customRange, setCustomRange] = useState<TimeRange | null>(null);
101
+ render: () => <WithTimezoneStory />,
102
+ };
103
+
104
+ function WithLiveModeStory() {
105
+ const [preset, setPreset] = useState<DateRangePreset | null>("15m");
106
+ const [customRange, setCustomRange] = useState<TimeRange | null>(null);
107
+ const [isLive, setIsLive] = useState(true);
108
+
109
+ return (
110
+ <TimeRangePicker
111
+ preset={customRange ? null : preset}
112
+ customRange={customRange}
113
+ onPresetChange={(p) => {
114
+ setPreset(p);
115
+ setCustomRange(null);
116
+ }}
117
+ onCustomRangeChange={(from, to) => {
118
+ setCustomRange({ from, to });
119
+ setPreset(null);
120
+ }}
121
+ onClearCustomRange={() => {
122
+ setCustomRange(null);
123
+ setPreset("15m");
124
+ }}
125
+ showLive
126
+ isLive={isLive}
127
+ onLiveChange={setIsLive}
128
+ />
129
+ );
130
+ }
77
131
 
78
- return (
132
+ /**
133
+ * With LIVE mode toggle enabled.
134
+ */
135
+ export const WithLiveMode: Story = {
136
+ render: () => <WithLiveModeStory />,
137
+ };
138
+
139
+ /**
140
+ * Disabled state.
141
+ */
142
+ export const Disabled: Story = {
143
+ args: {
144
+ preset: "7d",
145
+ disabled: true,
146
+ },
147
+ };
148
+
149
+ function DatadogStyleStory() {
150
+ const [preset, setPreset] = useState<DateRangePreset | null>("7d");
151
+ const [customRange, setCustomRange] = useState<TimeRange | null>(null);
152
+ const [customLabel, setCustomLabel] = useState<string | null>(null);
153
+ const [isLive, setIsLive] = useState(false);
154
+
155
+ return (
156
+ <div className="space-y-4">
79
157
  <TimeRangePicker
80
158
  preset={customRange ? null : preset}
81
159
  customRange={customRange}
160
+ customRangeLabel={customLabel}
82
161
  onPresetChange={(p) => {
83
162
  setPreset(p);
84
163
  setCustomRange(null);
164
+ setCustomLabel(null);
85
165
  }}
86
- onCustomRangeChange={(from, to) => {
166
+ onCustomRangeChange={(from, to, label) => {
87
167
  setCustomRange({ from, to });
88
168
  setPreset(null);
169
+ setCustomLabel(label || null);
89
170
  }}
90
171
  onClearCustomRange={() => {
91
172
  setCustomRange(null);
92
- setPreset("30d");
173
+ setPreset("7d");
174
+ setCustomLabel(null);
93
175
  }}
176
+ showLive
177
+ isLive={isLive}
178
+ onLiveChange={setIsLive}
94
179
  timezone="UTC-08:00"
95
180
  />
96
- );
97
- },
98
- };
181
+ <div className="rounded-md bg-muted p-3 text-xs text-muted-foreground">
182
+ <strong>Current state:</strong>
183
+ <pre className="mt-1 overflow-auto">
184
+ {JSON.stringify(
185
+ {
186
+ preset,
187
+ customRange: customRange
188
+ ? {
189
+ from: customRange.from.toISOString(),
190
+ to: customRange.to.toISOString(),
191
+ }
192
+ : null,
193
+ customLabel,
194
+ isLive,
195
+ },
196
+ null,
197
+ 2,
198
+ )}
199
+ </pre>
200
+ </div>
201
+ </div>
202
+ );
203
+ }
99
204
 
100
205
  /**
101
- * With LIVE mode toggle enabled.
206
+ * Full Datadog-style configuration with all features.
207
+ * Type natural language like "3 days ago", "last Wednesday", "past 2 weeks".
102
208
  */
103
- export const WithLiveMode: Story = {
104
- render: () => {
105
- const [preset, setPreset] = useState<DateRangePreset | null>("15m");
106
- const [customRange, setCustomRange] = useState<TimeRange | null>(null);
107
- const [isLive, setIsLive] = useState(true);
209
+ export const DatadogStyle: Story = {
210
+ render: () => <DatadogStyleStory />,
211
+ };
108
212
 
109
- return (
213
+ function NaturalLanguageParsingStory() {
214
+ const [preset, setPreset] = useState<DateRangePreset | null>("30d");
215
+ const [customRange, setCustomRange] = useState<TimeRange | null>(null);
216
+ const [customLabel, setCustomLabel] = useState<string | null>(null);
217
+
218
+ return (
219
+ <div className="space-y-4">
220
+ <p className="text-sm text-muted-foreground">
221
+ Try typing: "yesterday", "3 days ago", "last Wednesday", "January"
222
+ </p>
110
223
  <TimeRangePicker
111
224
  preset={customRange ? null : preset}
112
225
  customRange={customRange}
226
+ customRangeLabel={customLabel}
113
227
  onPresetChange={(p) => {
114
228
  setPreset(p);
115
229
  setCustomRange(null);
230
+ setCustomLabel(null);
116
231
  }}
117
- onCustomRangeChange={(from, to) => {
232
+ onCustomRangeChange={(from, to, label) => {
118
233
  setCustomRange({ from, to });
119
234
  setPreset(null);
235
+ setCustomLabel(label || null);
236
+ console.log("AI parsed:", { from, to, label });
120
237
  }}
121
238
  onClearCustomRange={() => {
122
239
  setCustomRange(null);
123
- setPreset("15m");
240
+ setPreset("30d");
241
+ setCustomLabel(null);
124
242
  }}
125
- showLive
126
- isLive={isLive}
127
- onLiveChange={setIsLive}
128
243
  />
129
- );
130
- },
131
- };
132
-
133
- /**
134
- * Disabled state.
135
- */
136
- export const Disabled: Story = {
137
- args: {
138
- preset: "7d",
139
- disabled: true,
140
- },
141
- };
142
-
143
- /**
144
- * Full Datadog-style configuration with all features.
145
- * Type natural language like "3 days ago", "last Wednesday", "past 2 weeks".
146
- */
147
- export const DatadogStyle: Story = {
148
- render: () => {
149
- const [preset, setPreset] = useState<DateRangePreset | null>("7d");
150
- const [customRange, setCustomRange] = useState<TimeRange | null>(null);
151
- const [customLabel, setCustomLabel] = useState<string | null>(null);
152
- const [isLive, setIsLive] = useState(false);
153
-
154
- return (
155
- <div className="space-y-4">
156
- <TimeRangePicker
157
- preset={customRange ? null : preset}
158
- customRange={customRange}
159
- customRangeLabel={customLabel}
160
- onPresetChange={(p) => {
161
- setPreset(p);
162
- setCustomRange(null);
163
- setCustomLabel(null);
164
- }}
165
- onCustomRangeChange={(from, to, label) => {
166
- setCustomRange({ from, to });
167
- setPreset(null);
168
- setCustomLabel(label || null);
169
- }}
170
- onClearCustomRange={() => {
171
- setCustomRange(null);
172
- setPreset("7d");
173
- setCustomLabel(null);
174
- }}
175
- showLive
176
- isLive={isLive}
177
- onLiveChange={setIsLive}
178
- timezone="UTC-08:00"
179
- />
180
- <div className="rounded-md bg-muted p-3 text-xs text-muted-foreground">
181
- <strong>Current state:</strong>
182
- <pre className="mt-1 overflow-auto">
183
- {JSON.stringify(
184
- {
185
- preset,
186
- customRange: customRange
187
- ? {
188
- from: customRange.from.toISOString(),
189
- to: customRange.to.toISOString(),
190
- }
191
- : null,
192
- customLabel,
193
- isLive,
194
- },
195
- null,
196
- 2,
197
- )}
198
- </pre>
199
- </div>
200
- </div>
201
- );
202
- },
203
- };
244
+ </div>
245
+ );
246
+ }
204
247
 
205
248
  /**
206
249
  * Natural language parsing demo.
@@ -212,38 +255,5 @@ export const DatadogStyle: Story = {
212
255
  * - "January 2024"
213
256
  */
214
257
  export const NaturalLanguageParsing: Story = {
215
- render: () => {
216
- const [preset, setPreset] = useState<DateRangePreset | null>("30d");
217
- const [customRange, setCustomRange] = useState<TimeRange | null>(null);
218
- const [customLabel, setCustomLabel] = useState<string | null>(null);
219
-
220
- return (
221
- <div className="space-y-4">
222
- <p className="text-sm text-muted-foreground">
223
- Try typing: "yesterday", "3 days ago", "last Wednesday", "January"
224
- </p>
225
- <TimeRangePicker
226
- preset={customRange ? null : preset}
227
- customRange={customRange}
228
- customRangeLabel={customLabel}
229
- onPresetChange={(p) => {
230
- setPreset(p);
231
- setCustomRange(null);
232
- setCustomLabel(null);
233
- }}
234
- onCustomRangeChange={(from, to, label) => {
235
- setCustomRange({ from, to });
236
- setPreset(null);
237
- setCustomLabel(label || null);
238
- console.log("AI parsed:", { from, to, label });
239
- }}
240
- onClearCustomRange={() => {
241
- setCustomRange(null);
242
- setPreset("30d");
243
- setCustomLabel(null);
244
- }}
245
- />
246
- </div>
247
- );
248
- },
258
+ render: () => <NaturalLanguageParsingStory />,
249
259
  };
@@ -0,0 +1,57 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ // Capture the options passed to createOpenRouter so we can assert on the
4
+ // auth headers / baseURL the date picker sends to /chat/completions.
5
+ const createOpenRouterMock = vi.fn();
6
+
7
+ vi.mock("@openrouter/ai-sdk-provider", () => ({
8
+ createOpenRouter: (opts: { headers?: Record<string, string> }) => {
9
+ createOpenRouterMock(opts);
10
+ return { chat: () => ({}) };
11
+ },
12
+ }));
13
+
14
+ vi.mock("ai", () => ({
15
+ generateObject: vi.fn(async () => ({
16
+ object: {
17
+ from: "2026-01-01T00:00:00",
18
+ to: "2026-01-01T23:59:59",
19
+ label: "Jan 1",
20
+ },
21
+ })),
22
+ }));
23
+
24
+ // Avoid pulling Datadog RUM (and its window access) into the Node test env.
25
+ vi.mock("@/lib/errorTracking", () => ({ trackError: vi.fn() }));
26
+
27
+ import { parseWithAI } from "./time-range-picker";
28
+
29
+ describe("parseWithAI request auth", () => {
30
+ beforeEach(() => {
31
+ createOpenRouterMock.mockClear();
32
+ });
33
+
34
+ // Root-cause regression test: the /chat/completions proxy authenticates from
35
+ // request headers, so the session credential MUST be forwarded. Before the
36
+ // fix, parseWithAI sent only Gram-Project and relied on a cookie, so this
37
+ // header was absent and the request 401'd (silently).
38
+ it("forwards the session auth header and baseURL to the OpenRouter client", async () => {
39
+ await parseWithAI("yesterday", "https://app.getgram.ai", "proj-slug", {
40
+ "Gram-Session": "test-token",
41
+ });
42
+
43
+ expect(createOpenRouterMock).toHaveBeenCalledTimes(1);
44
+ const opts = createOpenRouterMock.mock.calls[0]![0];
45
+ expect(opts.baseURL).toBe("https://app.getgram.ai");
46
+ expect(opts.headers["Gram-Session"]).toBe("test-token");
47
+ expect(opts.headers["Gram-Project"]).toBe("proj-slug");
48
+ });
49
+
50
+ it("still sets Gram-Project when no auth headers are provided", async () => {
51
+ await parseWithAI("yesterday", "https://app.getgram.ai", "proj-slug");
52
+
53
+ const opts = createOpenRouterMock.mock.calls[0]![0];
54
+ expect(opts.headers["Gram-Project"]).toBe("proj-slug");
55
+ expect(opts.headers["Gram-Session"]).toBeUndefined();
56
+ });
57
+ });
@@ -6,6 +6,7 @@ import { createOpenRouter } from "@openrouter/ai-sdk-provider";
6
6
  import { z } from "zod";
7
7
 
8
8
  import { cn } from "@/lib/utils";
9
+ import { trackError } from "@/lib/errorTracking";
9
10
  import { Popover, PopoverContent, PopoverTrigger } from "./popover";
10
11
  import { Calendar } from "./calendar";
11
12
 
@@ -154,7 +155,7 @@ const BADGE_WIDTH = "min-w-10";
154
155
 
155
156
  export function getPresetRange(preset: DateRangePreset): TimeRange {
156
157
  const p = PRESETS.find((p) => p.value === preset);
157
- return p ? p.getRange() : PRESETS[5].getRange(); // Default to 3d
158
+ return p ? p.getRange() : PRESETS[5]!.getRange(); // Default to 3d
158
159
  }
159
160
 
160
161
  function getPresetByValue(value: DateRangePreset): TimeRangePreset | undefined {
@@ -187,11 +188,15 @@ function parseAsLocalDate(isoString: string): Date {
187
188
  // Try to extract just the date part and create a local date
188
189
  const dateMatch = isoString.match(/^(\d{4})-(\d{2})-(\d{2})/);
189
190
  if (dateMatch) {
190
- const [, year, month, day] = dateMatch;
191
+ const year = dateMatch[1]!;
192
+ const month = dateMatch[2]!;
193
+ const day = dateMatch[3]!;
191
194
  // Check if there's a time component
192
195
  const timeMatch = isoString.match(/T(\d{2}):(\d{2}):?(\d{2})?/);
193
196
  if (timeMatch) {
194
- const [, hours, minutes, seconds = "0"] = timeMatch;
197
+ const hours = timeMatch[1]!;
198
+ const minutes = timeMatch[2]!;
199
+ const seconds = timeMatch[3] ?? "0";
195
200
  return new Date(
196
201
  parseInt(year),
197
202
  parseInt(month) - 1,
@@ -208,16 +213,23 @@ function parseAsLocalDate(isoString: string): Date {
208
213
  return new Date(isoString);
209
214
  }
210
215
 
211
- async function parseWithAI(
216
+ // Exported for unit testing of header/credential construction.
217
+ export async function parseWithAI(
212
218
  input: string,
213
219
  apiUrl: string,
214
220
  projectSlug?: string,
221
+ authHeaders?: Record<string, string>,
215
222
  ): Promise<ParseResult> {
216
223
  try {
217
224
  const now = new Date();
218
225
 
219
- // Create OpenRouter provider without X-Gram-Source header (so usage is billed)
220
- const headers: Record<string, string> = {};
226
+ // Create OpenRouter provider without X-Gram-Source header (so usage is billed).
227
+ // authHeaders carries the caller's session credential (e.g. Gram-Session /
228
+ // Gram-Chat-Session). The /chat/completions proxy authenticates from these
229
+ // request headers — relying on cookies alone fails (the Vercel AI SDK does
230
+ // not forward them reliably), so a missing credential here 401s and the
231
+ // catch below would silently swallow it.
232
+ const headers: Record<string, string> = { ...authHeaders };
221
233
  if (projectSlug) {
222
234
  headers["Gram-Project"] = projectSlug;
223
235
  }
@@ -290,7 +302,15 @@ User input: ${input}`,
290
302
 
291
303
  // Use the semantic label from AI (e.g., "Mon", "Jan", "2024", "1/5-1/10")
292
304
  return { type: "custom", range: { from, to }, label: parsed.label };
293
- } catch {
305
+ } catch (error) {
306
+ // Returning null keeps the UI graceful on an unparseable string, but the
307
+ // failure must not be invisible — a 401/gateway error here looks identical
308
+ // to "couldn't parse" to the user. Surface it so it's diagnosable.
309
+ trackError(error, {
310
+ source: "custom",
311
+ location: "TimeRangePicker.parseWithAI",
312
+ hasAuthHeaders: Boolean(authHeaders),
313
+ });
294
314
  return null;
295
315
  }
296
316
  }
@@ -328,6 +348,13 @@ export interface TimeRangePickerProps {
328
348
  apiUrl?: string;
329
349
  /** Project slug for API authentication */
330
350
  projectSlug?: string;
351
+ /**
352
+ * Auth headers to send with the AI parsing request to /chat/completions
353
+ * (e.g. `{ "Gram-Session": token }`). The `/chat/completions` proxy
354
+ * authenticates from request headers, not cookies, so without these the
355
+ * request is rejected with 401 and natural-language parsing silently fails.
356
+ */
357
+ authHeaders?: Record<string, string>;
331
358
  /** Additional class name for the trigger */
332
359
  className?: string;
333
360
  }
@@ -347,8 +374,9 @@ function TimeRangePicker({
347
374
  timezone,
348
375
  apiUrl,
349
376
  projectSlug,
377
+ authHeaders,
350
378
  className,
351
- }: TimeRangePickerProps) {
379
+ }: TimeRangePickerProps): React.JSX.Element {
352
380
  const [isOpen, setIsOpen] = React.useState(false);
353
381
  const [showCalendar, setShowCalendar] = React.useState(false);
354
382
  const [inputValue, setInputValue] = React.useState("");
@@ -441,6 +469,7 @@ function TimeRangePicker({
441
469
  inputValue,
442
470
  effectiveApiUrl,
443
471
  projectSlug,
472
+ authHeaders,
444
473
  );
445
474
  applyParseResult(aiParsed);
446
475
  } finally {
@@ -560,7 +589,9 @@ function TimeRangePicker({
560
589
  onClick={handleInputClick}
561
590
  onFocus={handleInputFocus}
562
591
  onBlur={handleInputBlur}
563
- onKeyDown={handleInputKeyDown}
592
+ onKeyDown={(e) => {
593
+ void handleInputKeyDown(e);
594
+ }}
564
595
  placeholder="e.g., 3 days ago, last week..."
565
596
  disabled={disabled}
566
597
  className={cn(
@@ -159,7 +159,11 @@ function isStructuredContent(
159
159
  * Helper Components
160
160
  * -------------------------------------------------------------------------- */
161
161
 
162
- function StatusIndicator({ status }: { status: ToolStatus }) {
162
+ function StatusIndicator({
163
+ status,
164
+ }: {
165
+ status: ToolStatus;
166
+ }): React.JSX.Element {
163
167
  return (
164
168
  <div className={cn(statusVariants({ status }))}>
165
169
  {status === "pending" && null}
@@ -173,7 +177,7 @@ function StatusIndicator({ status }: { status: ToolStatus }) {
173
177
  );
174
178
  }
175
179
 
176
- function CopyButton({ content }: { content: string }) {
180
+ function CopyButton({ content }: { content: string }): React.JSX.Element {
177
181
  const [copied, setCopied] = useState(false);
178
182
 
179
183
  const handleCopy = async (e: React.MouseEvent) => {
@@ -185,7 +189,9 @@ function CopyButton({ content }: { content: string }) {
185
189
 
186
190
  return (
187
191
  <button
188
- onClick={handleCopy}
192
+ onClick={(e) => {
193
+ void handleCopy(e);
194
+ }}
189
195
  className="rounded p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
190
196
  aria-label="Copy to clipboard"
191
197
  >
@@ -226,7 +232,7 @@ function SyntaxHighlightedCode({
226
232
  text: string;
227
233
  language?: BundledLanguage;
228
234
  className?: string;
229
- }) {
235
+ }): React.JSX.Element {
230
236
  const [highlightedCode, setHighlightedCode] = useState<string | null>(null);
231
237
  const [expanded, setExpanded] = useState(false);
232
238
 
@@ -241,7 +247,7 @@ function SyntaxHighlightedCode({
241
247
  setHighlightedCode(null);
242
248
  if (!language || !canHighlight) return;
243
249
  let cancelled = false;
244
- codeToHtml(displayText, {
250
+ void codeToHtml(displayText, {
245
251
  lang: language,
246
252
  theme: "github-dark-default",
247
253
  rootStyle: "background-color: transparent;",
@@ -370,7 +376,7 @@ function ToolUISection({
370
376
  defaultExpanded = false,
371
377
  highlightSyntax = true,
372
378
  language = "json",
373
- }: ToolUISectionProps) {
379
+ }: ToolUISectionProps): React.JSX.Element {
374
380
  const [isExpanded, setIsExpanded] = useState(defaultExpanded);
375
381
 
376
382
  // For structured content, we don't stringify it
@@ -434,7 +440,7 @@ function ToolUI({
434
440
  onApproveOnce,
435
441
  onApproveForSession,
436
442
  onDeny,
437
- }: ToolUIProps) {
443
+ }: ToolUIProps): React.JSX.Element {
438
444
  // Use annotation title if available, otherwise fall back to name
439
445
  const displayName = annotations?.title || name;
440
446
  const isApprovalPending =
@@ -455,6 +461,7 @@ function ToolUI({
455
461
  if (!isApprovalPending && isExpanded && !defaultExpanded) {
456
462
  setIsExpanded(false);
457
463
  }
464
+ // oxlint-disable-next-line react-hooks/exhaustive-deps -- only react to approval transition; defaultExpanded/isExpanded are intentionally not deps
458
465
  }, [isApprovalPending]);
459
466
 
460
467
  // Handle approve based on selected mode
@@ -497,7 +504,9 @@ function ToolUI({
497
504
 
498
505
  {/* Tool row */}
499
506
  <button
500
- onClick={() => hasContent && setIsExpanded(!isExpanded)}
507
+ onClick={() => {
508
+ if (hasContent) setIsExpanded(!isExpanded);
509
+ }}
501
510
  disabled={!hasContent}
502
511
  className={cn(
503
512
  "flex w-full items-center gap-2 px-4 py-3 text-left",
@@ -715,7 +724,7 @@ function ToolUIGroup({
715
724
  defaultExpanded = false,
716
725
  children,
717
726
  className,
718
- }: ToolUIGroupProps) {
727
+ }: ToolUIGroupProps): React.JSX.Element {
719
728
  const [isExpanded, setIsExpanded] = useState(defaultExpanded);
720
729
 
721
730
  return (
@@ -9,7 +9,7 @@ import { usePortalContainer } from "@/hooks/usePortalContainer";
9
9
  function TooltipProvider({
10
10
  delayDuration = 0,
11
11
  ...props
12
- }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
12
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>): React.JSX.Element {
13
13
  return (
14
14
  <TooltipPrimitive.Provider
15
15
  data-slot="tooltip-provider"
@@ -21,7 +21,7 @@ function TooltipProvider({
21
21
 
22
22
  function Tooltip({
23
23
  ...props
24
- }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
24
+ }: React.ComponentProps<typeof TooltipPrimitive.Root>): React.JSX.Element {
25
25
  return (
26
26
  <TooltipProvider>
27
27
  <TooltipPrimitive.Root data-slot="tooltip" {...props} />
@@ -31,7 +31,7 @@ function Tooltip({
31
31
 
32
32
  function TooltipTrigger({
33
33
  ...props
34
- }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
34
+ }: React.ComponentProps<typeof TooltipPrimitive.Trigger>): React.JSX.Element {
35
35
  return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
36
36
  }
37
37
 
@@ -43,7 +43,7 @@ function TooltipContent({
43
43
  ...props
44
44
  }: React.ComponentProps<typeof TooltipPrimitive.Content> & {
45
45
  container?: HTMLElement | null;
46
- }) {
46
+ }): React.JSX.Element {
47
47
  const portalContainer = usePortalContainer();
48
48
  return (
49
49
  <TooltipPrimitive.Portal container={container ?? portalContainer}>
@@ -12,7 +12,7 @@ export const ChatIdContext = createContext<ChatIdContextValue | null>(null);
12
12
  *
13
13
  * @returns The current chat ID, or null if not yet initialized
14
14
  */
15
- export const useChatId = () => {
15
+ export const useChatId = (): string | null => {
16
16
  const context = useContext(ChatIdContext);
17
17
  if (!context) {
18
18
  throw new Error("useChatId must be used within ElementsProvider");