@gram-ai/elements 1.25.0 → 1.25.2

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 (50) hide show
  1. package/dist/components/Chat/stories/MessageFeedback.stories.d.ts +1 -1
  2. package/dist/components/Replay.stories.d.ts +16 -0
  3. package/dist/contexts/ChatIdContext.d.ts +11 -0
  4. package/dist/contexts/contexts.d.ts +1 -0
  5. package/dist/elements.cjs +1 -1
  6. package/dist/elements.css +1 -1
  7. package/dist/elements.js +7 -6
  8. package/dist/{index-iUSSoKFz.cjs → index-B8nSCdu4.cjs} +11 -11
  9. package/dist/index-B8nSCdu4.cjs.map +1 -0
  10. package/dist/{index-wBHCO1r-.cjs → index-CAtaLV1E.cjs} +64 -55
  11. package/dist/index-CAtaLV1E.cjs.map +1 -0
  12. package/dist/{index-CtyV0c-T.js → index-CJrwma08.js} +3737 -3730
  13. package/dist/index-CJrwma08.js.map +1 -0
  14. package/dist/{index-DDb23655.js → index-DLWQ91ow.js} +8494 -8418
  15. package/dist/index-DLWQ91ow.js.map +1 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/lib/messageConverter.d.ts +1 -1
  18. package/dist/lib/messageConverter.test.d.ts +1 -0
  19. package/dist/plugins.cjs +1 -1
  20. package/dist/plugins.js +1 -1
  21. package/dist/{profiler-CGIJBY8c.js → profiler-BaG0scxd.js} +2 -2
  22. package/dist/{profiler-CGIJBY8c.js.map → profiler-BaG0scxd.js.map} +1 -1
  23. package/dist/{profiler-CLtQEzfv.cjs → profiler-CuqENACf.cjs} +2 -2
  24. package/dist/{profiler-CLtQEzfv.cjs.map → profiler-CuqENACf.cjs.map} +1 -1
  25. package/dist/{startRecording-x0G7lOpP.js → startRecording-86bHmd-l.js} +2 -2
  26. package/dist/{startRecording-x0G7lOpP.js.map → startRecording-86bHmd-l.js.map} +1 -1
  27. package/dist/{startRecording-DXZPNn9e.cjs → startRecording-BiLmoqZa.cjs} +2 -2
  28. package/dist/{startRecording-DXZPNn9e.cjs.map → startRecording-BiLmoqZa.cjs.map} +1 -1
  29. package/dist/types/index.d.ts +4 -4
  30. package/package.json +1 -1
  31. package/src/components/Chat/stories/MessageFeedback.stories.tsx +6 -6
  32. package/src/components/Chat/stories/ToolApproval.stories.tsx +10 -10
  33. package/src/components/Chat/stories/Tools.stories.tsx +122 -104
  34. package/src/components/Chat/stories/Variants.stories.tsx +1 -1
  35. package/src/components/Replay.stories.tsx +230 -0
  36. package/src/components/ShadowRoot.tsx +5 -1
  37. package/src/components/assistant-ui/message-feedback.tsx +6 -7
  38. package/src/components/assistant-ui/thread.tsx +76 -11
  39. package/src/contexts/ChatIdContext.tsx +21 -0
  40. package/src/contexts/ElementsProvider.tsx +77 -37
  41. package/src/contexts/contexts.ts +2 -0
  42. package/src/hooks/useAuth.ts +1 -2
  43. package/src/index.ts +1 -0
  44. package/src/lib/messageConverter.test.ts +242 -0
  45. package/src/lib/messageConverter.ts +22 -10
  46. package/src/types/index.ts +4 -4
  47. package/dist/index-CtyV0c-T.js.map +0 -1
  48. package/dist/index-DDb23655.js.map +0 -1
  49. package/dist/index-iUSSoKFz.cjs.map +0 -1
  50. package/dist/index-wBHCO1r-.cjs.map +0 -1
@@ -1,8 +1,9 @@
1
1
  import { ToolCallMessagePartProps } from '@assistant-ui/react'
2
2
  import type { Meta, StoryFn } from '@storybook/react-vite'
3
- import React from 'react'
3
+ import React, { useState, useCallback } from 'react'
4
4
  import z from 'zod'
5
5
  import { Chat } from '..'
6
+ import { useToolExecution } from '../../../contexts/ToolExecutionContext'
6
7
  import { defineFrontendTool } from '../../../lib/tools'
7
8
 
8
9
  const meta: Meta<typeof Chat> = {
@@ -17,128 +18,145 @@ export default meta
17
18
 
18
19
  type Story = StoryFn<typeof Chat>
19
20
 
20
- const CardPinRevealComponent = ({
21
- result,
22
- argsText,
23
- }: ToolCallMessagePartProps) => {
24
- const [isFlipped, setIsFlipped] = React.useState(false)
21
+ const ProductCardComponent = ({ result }: ToolCallMessagePartProps) => {
22
+ const { executeTool, isToolAvailable } = useToolExecution()
23
+ const [isLoading, setIsLoading] = useState(false)
24
+ const [addedToCart, setAddedToCart] = useState(false)
25
+
26
+ // Parse the result to get product details
27
+ let product = {
28
+ id: '',
29
+ name: 'Loading...',
30
+ description: '',
31
+ price: 0,
32
+ category: '',
33
+ rating: 0,
34
+ reviewCount: 0,
35
+ imageUrl: '',
36
+ inStock: true,
37
+ }
25
38
 
26
- // Parse the result to get the pin
27
- let pin = '****'
28
39
  try {
29
40
  if (result) {
30
41
  const parsed = typeof result === 'string' ? JSON.parse(result) : result
31
42
  if (parsed?.content?.[0]?.text) {
32
43
  const content = JSON.parse(parsed.content[0].text)
33
- pin = content.pin || '****'
34
- } else if (parsed?.pin) {
35
- pin = parsed.pin
44
+ product = { ...product, ...content }
45
+ } else if (parsed?.name) {
46
+ product = { ...product, ...parsed }
36
47
  }
37
48
  }
38
49
  } catch {
39
50
  // Fallback to default
40
51
  }
41
52
 
42
- const args = JSON.parse(argsText || '{}')
43
- const cardNumber = args?.queryParameters?.cardNumber || '4532 •••• •••• 1234'
44
- const cardHolder = 'JOHN DOE'
45
- const expiry = '12/25'
46
- const cvv = '123'
53
+ const canAddToCart = isToolAvailable('ecommerce_api_add_to_cart')
47
54
 
48
- if (!cardNumber) {
49
- return null
50
- }
55
+ const handleAddToCart = useCallback(async () => {
56
+ if (!product.id || !canAddToCart) return
51
57
 
52
- return (
53
- <div className="my-4 perspective-[1000px]">
54
- <div
55
- className={`relative h-48 w-80 cursor-pointer transition-transform duration-700 [transform-style:preserve-3d] ${
56
- isFlipped ? 'transform-[rotateY(180deg)]' : ''
57
- }`}
58
- onClick={() => setIsFlipped(!isFlipped)}
59
- >
60
- {/* Front of card */}
61
- <div className="absolute inset-0 backface-hidden">
62
- <div className="relative h-full w-full overflow-hidden rounded-xl bg-gradient-to-br from-indigo-600 via-purple-600 to-pink-500 p-6 text-white shadow-2xl">
63
- {/* Card pattern overlay */}
64
- <div className="absolute inset-0 opacity-10">
65
- <div className="absolute -top-10 -right-10 h-40 w-40 rounded-full bg-white"></div>
66
- <div className="absolute -bottom-10 -left-10 h-32 w-32 rounded-full bg-white"></div>
67
- </div>
58
+ setIsLoading(true)
59
+ try {
60
+ // HTTP tools from OpenAPI expect body content wrapped in a 'body' field
61
+ const toolResult = await executeTool('ecommerce_api_add_to_cart', {
62
+ body: {
63
+ productId: product.id,
64
+ quantity: 1,
65
+ },
66
+ })
68
67
 
69
- {/* Card content */}
70
- <div className="relative z-10 flex h-full flex-col justify-between">
71
- <div className="flex items-center justify-between">
72
- <div className="text-2xl font-bold">VISA</div>
73
- <div className="h-8 w-12 rounded bg-white/20"></div>
74
- </div>
68
+ if (toolResult.success) {
69
+ setAddedToCart(true)
70
+ } else {
71
+ console.error('[ProductCard] Tool failed:', toolResult.error)
72
+ }
73
+ } catch (err) {
74
+ console.error('[ProductCard] Exception:', err)
75
+ } finally {
76
+ setIsLoading(false)
77
+ }
78
+ }, [product.id, canAddToCart, executeTool])
75
79
 
76
- <div className="space-y-2">
77
- <div className="font-mono text-2xl tracking-wider">
78
- {cardNumber}
79
- </div>
80
- <div className="flex items-center justify-between text-sm">
81
- <div>
82
- <div className="text-xs opacity-70">CARDHOLDER</div>
83
- <div className="font-semibold">{cardHolder}</div>
84
- </div>
85
- <div>
86
- <div className="text-xs opacity-70">EXPIRES</div>
87
- <div className="font-semibold">{expiry}</div>
88
- </div>
89
- </div>
90
- </div>
80
+ return (
81
+ <div className="my-4 w-80">
82
+ <div className="overflow-hidden rounded-xl bg-white shadow-lg dark:bg-slate-800">
83
+ {/* Product Image */}
84
+ <div className="relative h-48 bg-gradient-to-br from-indigo-100 to-purple-100 dark:from-indigo-900 dark:to-purple-900">
85
+ {product.imageUrl ? (
86
+ <img
87
+ src={product.imageUrl}
88
+ alt={product.name}
89
+ className="h-full w-full object-cover"
90
+ />
91
+ ) : (
92
+ <div className="flex h-full items-center justify-center">
93
+ <span className="text-6xl">📦</span>
91
94
  </div>
92
-
93
- {/* Click hint */}
94
- <div className="absolute right-2 bottom-2 text-xs opacity-50">
95
- Click to flip
95
+ )}
96
+ {!product.inStock && (
97
+ <div className="absolute top-2 right-2 rounded-full bg-red-500 px-2 py-1 text-xs font-semibold text-white">
98
+ Out of Stock
96
99
  </div>
97
- </div>
100
+ )}
98
101
  </div>
99
102
 
100
- {/* Back of card */}
101
- <div className="absolute inset-0 transform-[rotateY(180deg)] backface-hidden">
102
- <div className="relative h-full w-full overflow-hidden rounded-xl bg-gradient-to-br from-slate-800 via-slate-700 to-slate-900 p-6 text-white shadow-2xl">
103
- {/* Magnetic strip */}
104
- <div className="absolute top-8 right-0 left-0 h-12 bg-black"></div>
105
-
106
- {/* Card content */}
107
- <div className="relative z-10 flex h-full flex-col justify-between">
108
- <div className="mt-16 space-y-4">
109
- <div className="flex items-center gap-2">
110
- <div className="h-8 flex-1 rounded bg-white/10 px-3 py-2 text-right font-mono text-sm">
111
- {cvv}
112
- </div>
113
- <div className="text-xs opacity-70">CVV</div>
114
- </div>
115
-
116
- {/* PIN Display */}
117
- <div className="mt-6 space-y-2">
118
- <div className="text-xs opacity-70">PIN</div>
119
- <div className="flex items-center gap-3">
120
- <div className="flex h-16 w-16 items-center justify-center rounded-lg bg-gradient-to-br from-yellow-400 to-orange-500 shadow-lg">
121
- <span className="text-2xl font-bold text-white">
122
- {pin}
123
- </span>
124
- </div>
125
- <div className="text-xs opacity-60">
126
- Keep this PIN secure
127
- </div>
128
- </div>
129
- </div>
130
- </div>
103
+ {/* Product Details */}
104
+ <div className="p-4">
105
+ <div className="mb-1 text-xs font-medium tracking-wide text-indigo-500 uppercase dark:text-indigo-400">
106
+ {product.category}
107
+ </div>
108
+ <h3 className="mb-2 text-lg font-bold text-slate-900 dark:text-white">
109
+ {product.name}
110
+ </h3>
111
+ <p className="mb-3 line-clamp-2 text-sm text-slate-600 dark:text-slate-300">
112
+ {product.description}
113
+ </p>
131
114
 
132
- <div className="flex items-center justify-between text-xs opacity-50">
133
- <div>VISA</div>
134
- <div>{cardNumber}</div>
135
- </div>
115
+ {/* Rating */}
116
+ <div className="mb-3 flex items-center gap-1">
117
+ <div className="flex">
118
+ {[1, 2, 3, 4, 5].map((star) => (
119
+ <span
120
+ key={star}
121
+ className={
122
+ star <= Math.round(product.rating)
123
+ ? 'text-yellow-400'
124
+ : 'text-slate-300'
125
+ }
126
+ >
127
+
128
+ </span>
129
+ ))}
136
130
  </div>
131
+ <span className="text-sm text-slate-500">
132
+ ({product.reviewCount} reviews)
133
+ </span>
134
+ </div>
137
135
 
138
- {/* Click hint */}
139
- <div className="absolute bottom-2 left-2 text-xs opacity-50">
140
- Click to flip back
141
- </div>
136
+ {/* Price and Add to Cart */}
137
+ <div className="flex items-center justify-between">
138
+ <span className="text-2xl font-bold text-slate-900 dark:text-white">
139
+ ${product.price?.toFixed(2)}
140
+ </span>
141
+ <button
142
+ onClick={handleAddToCart}
143
+ disabled={
144
+ isLoading || addedToCart || !canAddToCart || !product.inStock
145
+ }
146
+ className={`rounded-lg px-4 py-2 text-sm font-semibold text-white transition-colors ${
147
+ addedToCart
148
+ ? 'bg-green-500'
149
+ : isLoading
150
+ ? 'bg-indigo-400'
151
+ : 'bg-indigo-600 hover:bg-indigo-700'
152
+ } disabled:cursor-not-allowed disabled:opacity-50`}
153
+ >
154
+ {addedToCart
155
+ ? '✓ Added'
156
+ : isLoading
157
+ ? 'Adding...'
158
+ : 'Add to Cart'}
159
+ </button>
142
160
  </div>
143
161
  </div>
144
162
  </div>
@@ -154,15 +172,15 @@ CustomToolComponent.parameters = {
154
172
  welcome: {
155
173
  suggestions: [
156
174
  {
157
- title: 'Get card details',
158
- label: 'for your card',
159
- prompt: 'Get card details for your card number 4532 •••• •••• 1234',
175
+ title: 'Get product details',
176
+ label: 'View a product',
177
+ prompt: 'List products and then show me details for the first one',
160
178
  },
161
179
  ],
162
180
  },
163
181
  tools: {
164
182
  components: {
165
- kitchen_sink_get_get_card_details: CardPinRevealComponent,
183
+ ecommerce_api_get_product: ProductCardComponent,
166
184
  },
167
185
  },
168
186
  },
@@ -61,7 +61,7 @@ StandaloneWithHistory.parameters = {
61
61
  history: { enabled: true, showThreadList: true },
62
62
  model: { showModelPicker: true },
63
63
  tools: {
64
- toolsRequiringApproval: ['kitchen_sink_get_salutation'],
64
+ toolsRequiringApproval: ['ecommerce_api_create_order'],
65
65
  },
66
66
  },
67
67
  },
@@ -141,6 +141,178 @@ const reasoningCassette: Cassette = {
141
141
  ],
142
142
  }
143
143
 
144
+ /**
145
+ * Simulates what the converter produced BEFORE the dedup fix (AGE-1295).
146
+ *
147
+ * The server accumulates all tool calls into each assistant message, so when
148
+ * assistant-ui merges consecutive assistant chunks into a single displayed
149
+ * message, the parts look like:
150
+ * chunk 1: text + tc_1
151
+ * chunk 2: text + tc_1 + tc_2 ← tc_1 duplicated
152
+ * chunk 3: text + tc_1 + tc_2 + tc_3 ← tc_1, tc_2 duplicated
153
+ *
154
+ * This causes every tool group to show "Executed 3 tools" instead of 1 each.
155
+ */
156
+ const beforeFixCassette: Cassette = {
157
+ messages: [
158
+ {
159
+ role: 'user',
160
+ content: [
161
+ { type: 'text', text: 'Search for 3 deals in HubSpot for me.' },
162
+ ],
163
+ },
164
+ {
165
+ role: 'assistant',
166
+ content: [
167
+ // --- chunk 1: first attempt ---
168
+ {
169
+ type: 'text',
170
+ text: "I'll search for 3 deals in HubSpot for you.",
171
+ },
172
+ {
173
+ type: 'tool-call',
174
+ toolCallId: 'tc_deals_1',
175
+ toolName: 'hubspot_search_deals',
176
+ args: { query: 'deals', limit: 3 },
177
+ result: { error: 'Invalid filter groups structure' },
178
+ },
179
+ // --- chunk 2: second attempt (accumulates tc_1 + tc_2) ---
180
+ {
181
+ type: 'text',
182
+ text: 'Let me try a different approach to retrieve deals:',
183
+ },
184
+ {
185
+ type: 'tool-call',
186
+ toolCallId: 'tc_deals_1_dup',
187
+ toolName: 'hubspot_search_deals',
188
+ args: { query: 'deals', limit: 3 },
189
+ result: { error: 'Invalid filter groups structure' },
190
+ },
191
+ {
192
+ type: 'tool-call',
193
+ toolCallId: 'tc_deals_2',
194
+ toolName: 'hubspot_search_deals',
195
+ args: { query: 'deals', limit: 3, filterGroups: [] },
196
+ result: { error: 'Filter groups must not be empty' },
197
+ },
198
+ // --- chunk 3: third attempt (accumulates tc_1 + tc_2 + tc_3) ---
199
+ {
200
+ type: 'text',
201
+ text: 'Let me try with proper filter groups structure:',
202
+ },
203
+ {
204
+ type: 'tool-call',
205
+ toolCallId: 'tc_deals_1_dup2',
206
+ toolName: 'hubspot_search_deals',
207
+ args: { query: 'deals', limit: 3 },
208
+ result: { error: 'Invalid filter groups structure' },
209
+ },
210
+ {
211
+ type: 'tool-call',
212
+ toolCallId: 'tc_deals_2_dup',
213
+ toolName: 'hubspot_search_deals',
214
+ args: { query: 'deals', limit: 3, filterGroups: [] },
215
+ result: { error: 'Filter groups must not be empty' },
216
+ },
217
+ {
218
+ type: 'tool-call',
219
+ toolCallId: 'tc_deals_3',
220
+ toolName: 'hubspot_search_deals',
221
+ args: {
222
+ limit: 3,
223
+ filterGroups: [
224
+ {
225
+ filters: [
226
+ { propertyName: 'dealname', operator: 'HAS_PROPERTY' },
227
+ ],
228
+ },
229
+ ],
230
+ },
231
+ result: {
232
+ deals: [
233
+ { id: '1', name: 'Acme Corp', amount: 50000 },
234
+ { id: '2', name: 'Globex Inc', amount: 75000 },
235
+ { id: '3', name: 'Initech LLC', amount: 30000 },
236
+ ],
237
+ },
238
+ },
239
+ {
240
+ type: 'text',
241
+ text: 'Here are the 3 deals I found:\n\n1. **Acme Corp** — $50,000\n2. **Globex Inc** — $75,000\n3. **Initech LLC** — $30,000',
242
+ },
243
+ ],
244
+ },
245
+ ],
246
+ }
247
+
248
+ const interleavedToolCallsCassette: Cassette = {
249
+ messages: [
250
+ {
251
+ role: 'user',
252
+ content: [
253
+ { type: 'text', text: 'Search for 3 deals in HubSpot for me.' },
254
+ ],
255
+ },
256
+ {
257
+ role: 'assistant',
258
+ content: [
259
+ {
260
+ type: 'text',
261
+ text: "I'll search for 3 deals in HubSpot for you.",
262
+ },
263
+ {
264
+ type: 'tool-call',
265
+ toolCallId: 'tc_deals_1',
266
+ toolName: 'hubspot_search_deals',
267
+ args: { query: 'deals', limit: 3 },
268
+ result: { error: 'Invalid filter groups structure' },
269
+ },
270
+ {
271
+ type: 'text',
272
+ text: 'Let me try a different approach to retrieve deals:',
273
+ },
274
+ {
275
+ type: 'tool-call',
276
+ toolCallId: 'tc_deals_2',
277
+ toolName: 'hubspot_search_deals',
278
+ args: { query: 'deals', limit: 3, filterGroups: [] },
279
+ result: { error: 'Filter groups must not be empty' },
280
+ },
281
+ {
282
+ type: 'text',
283
+ text: 'Let me try with proper filter groups structure:',
284
+ },
285
+ {
286
+ type: 'tool-call',
287
+ toolCallId: 'tc_deals_3',
288
+ toolName: 'hubspot_search_deals',
289
+ args: {
290
+ limit: 3,
291
+ filterGroups: [
292
+ {
293
+ filters: [
294
+ { propertyName: 'dealname', operator: 'HAS_PROPERTY' },
295
+ ],
296
+ },
297
+ ],
298
+ },
299
+ result: {
300
+ deals: [
301
+ { id: '1', name: 'Acme Corp', amount: 50000 },
302
+ { id: '2', name: 'Globex Inc', amount: 75000 },
303
+ { id: '3', name: 'Initech LLC', amount: 30000 },
304
+ ],
305
+ },
306
+ },
307
+ {
308
+ type: 'text',
309
+ text: 'Here are the 3 deals I found:\n\n1. **Acme Corp** — $50,000\n2. **Globex Inc** — $75,000\n3. **Initech LLC** — $30,000',
310
+ },
311
+ ],
312
+ },
313
+ ],
314
+ }
315
+
144
316
  const multiTurnCassette: Cassette = {
145
317
  messages: [
146
318
  {
@@ -240,6 +412,64 @@ ToolCalls.decorators = [
240
412
  ),
241
413
  ]
242
414
 
415
+ /**
416
+ * BEFORE the fix (AGE-1295): simulates the duplicated tool calls that the
417
+ * converter used to produce. Each tool group shows an inflated count because
418
+ * the server's accumulated tool_calls were not deduplicated.
419
+ *
420
+ * Expected: groups show "Executed 2 tools" and "Executed 3 tools" instead
421
+ * of 1 tool each — this is the bug.
422
+ */
423
+ export const BeforeFix_DuplicatedToolCalls: Story = () => (
424
+ <Replay
425
+ cassette={beforeFixCassette}
426
+ config={{
427
+ variant: 'standalone',
428
+ tools: { expandToolGroupsByDefault: false },
429
+ }}
430
+ typingSpeed={0}
431
+ userMessageDelay={0}
432
+ assistantStartDelay={0}
433
+ >
434
+ <Chat />
435
+ </Replay>
436
+ )
437
+ BeforeFix_DuplicatedToolCalls.decorators = [
438
+ (Story) => (
439
+ <div className="m-auto flex h-screen w-full max-w-3xl flex-col px-4">
440
+ <Story />
441
+ </div>
442
+ ),
443
+ ]
444
+
445
+ /**
446
+ * AFTER the fix (AGE-1295): the converter now deduplicates tool calls, so
447
+ * each tool group correctly shows only its own tool call.
448
+ *
449
+ * Expected: each group shows exactly 1 tool call.
450
+ */
451
+ export const AfterFix_DeduplicatedToolCalls: Story = () => (
452
+ <Replay
453
+ cassette={interleavedToolCallsCassette}
454
+ config={{
455
+ variant: 'standalone',
456
+ tools: { expandToolGroupsByDefault: false },
457
+ }}
458
+ typingSpeed={0}
459
+ userMessageDelay={0}
460
+ assistantStartDelay={0}
461
+ >
462
+ <Chat />
463
+ </Replay>
464
+ )
465
+ AfterFix_DeduplicatedToolCalls.decorators = [
466
+ (Story) => (
467
+ <div className="m-auto flex h-screen w-full max-w-3xl flex-col px-4">
468
+ <Story />
469
+ </div>
470
+ ),
471
+ ]
472
+
243
473
  /**
244
474
  * Replay with reasoning (chain-of-thought) content. The assistant's
245
475
  * internal reasoning is shown before the final response.
@@ -69,7 +69,11 @@ export const ShadowRoot = ({
69
69
  }, [shadowRoot, elementsStyles])
70
70
 
71
71
  return (
72
- <div ref={hostRef} className={hostClassName} style={hostStyle}>
72
+ <div
73
+ ref={hostRef}
74
+ className={hostClassName}
75
+ style={{ isolation: 'isolate', ...hostStyle }}
76
+ >
73
77
  {shadowRoot
74
78
  ? createPortal(
75
79
  <div
@@ -1,16 +1,15 @@
1
- import { X, Heart } from 'lucide-react'
2
- import * as m from 'motion/react-m'
3
- import { useState, type FC } from 'react'
4
- import { AnimatePresence } from 'motion/react'
5
-
6
- import { cn } from '@/lib/utils'
7
- import { EASE_OUT_QUINT } from '@/lib/easing'
8
1
  import {
9
2
  Tooltip,
10
3
  TooltipContent,
11
4
  TooltipProvider,
12
5
  TooltipTrigger,
13
6
  } from '@/components/ui/tooltip'
7
+ import { EASE_OUT_QUINT } from '@/lib/easing'
8
+ import { cn } from '@/lib/utils'
9
+ import { Heart, X } from 'lucide-react'
10
+ import { AnimatePresence } from 'motion/react'
11
+ import * as m from 'motion/react-m'
12
+ import { useState, type FC } from 'react'
14
13
 
15
14
  export type FeedbackType = 'dislike' | 'like'
16
15