xertica-ui 1.0.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 (141) hide show
  1. package/App.tsx +182 -0
  2. package/README.md +330 -0
  3. package/assets/xertica-logo.svg +38 -0
  4. package/assets/xertica-x-logo.svg +21 -0
  5. package/bin/cli.ts +193 -0
  6. package/components/AssistenteXertica.tsx +2003 -0
  7. package/components/AudioPlayer.tsx +203 -0
  8. package/components/CodeBlock.tsx +242 -0
  9. package/components/DocumentEditor.tsx +504 -0
  10. package/components/ForgotPasswordPage.tsx +170 -0
  11. package/components/FormattedDocument.tsx +87 -0
  12. package/components/HomeContent.tsx +123 -0
  13. package/components/HomePage.tsx +70 -0
  14. package/components/LanguageSelector.tsx +54 -0
  15. package/components/LoginPage.tsx +199 -0
  16. package/components/MarkdownMessage.tsx +62 -0
  17. package/components/ModernChatInput.tsx +502 -0
  18. package/components/PodcastPlayer.tsx +409 -0
  19. package/components/ResetPasswordPage.tsx +234 -0
  20. package/components/Sidebar.tsx +489 -0
  21. package/components/TemplateContent.tsx +629 -0
  22. package/components/TemplatePage.tsx +70 -0
  23. package/components/ThemeToggle.tsx +65 -0
  24. package/components/VerifyEmailPage.tsx +187 -0
  25. package/components/XerticaLogo.tsx +69 -0
  26. package/components/XerticaOrbe.tsx +1339 -0
  27. package/components/XerticaXLogo.tsx +53 -0
  28. package/components/examples/DrawingMapExample.tsx +530 -0
  29. package/components/examples/FilterableMapExample.tsx +380 -0
  30. package/components/examples/LocationPickerExample.tsx +330 -0
  31. package/components/examples/MapExamples.tsx +280 -0
  32. package/components/examples/MapShowcase.tsx +446 -0
  33. package/components/examples/RouteMapExamples.tsx +329 -0
  34. package/components/examples/SimpleFilterableMap.tsx +192 -0
  35. package/components/examples/index.ts +52 -0
  36. package/components/figma/ImageWithFallback.tsx +27 -0
  37. package/components/index.ts +44 -0
  38. package/components/media/AudioPlayer.tsx +278 -0
  39. package/components/media/FloatingMediaWrapper.tsx +166 -0
  40. package/components/media/VideoPlayer.tsx +285 -0
  41. package/components/ui/accordion.tsx +66 -0
  42. package/components/ui/alert-dialog.tsx +159 -0
  43. package/components/ui/alert.tsx +91 -0
  44. package/components/ui/aspect-ratio.tsx +11 -0
  45. package/components/ui/avatar.tsx +65 -0
  46. package/components/ui/badge.tsx +55 -0
  47. package/components/ui/breadcrumb.tsx +109 -0
  48. package/components/ui/button.tsx +78 -0
  49. package/components/ui/calendar.tsx +235 -0
  50. package/components/ui/card.tsx +92 -0
  51. package/components/ui/carousel.tsx +241 -0
  52. package/components/ui/chart.tsx +353 -0
  53. package/components/ui/checkbox.tsx +32 -0
  54. package/components/ui/collapsible.tsx +33 -0
  55. package/components/ui/command.tsx +177 -0
  56. package/components/ui/context-menu.tsx +252 -0
  57. package/components/ui/dialog.tsx +138 -0
  58. package/components/ui/drawer.tsx +134 -0
  59. package/components/ui/dropdown-menu.tsx +257 -0
  60. package/components/ui/empty.tsx +90 -0
  61. package/components/ui/file-upload.tsx +152 -0
  62. package/components/ui/form.tsx +195 -0
  63. package/components/ui/google-maps-loader.tsx +379 -0
  64. package/components/ui/hover-card.tsx +44 -0
  65. package/components/ui/index.ts +242 -0
  66. package/components/ui/input-otp.tsx +77 -0
  67. package/components/ui/input.tsx +38 -0
  68. package/components/ui/label.tsx +24 -0
  69. package/components/ui/map-config.ts +12 -0
  70. package/components/ui/map-layers.tsx +129 -0
  71. package/components/ui/map.exports.ts +31 -0
  72. package/components/ui/map.tsx +412 -0
  73. package/components/ui/menubar.tsx +276 -0
  74. package/components/ui/navigation-menu.tsx +162 -0
  75. package/components/ui/notification-badge.tsx +61 -0
  76. package/components/ui/page-header.tsx +229 -0
  77. package/components/ui/pagination.tsx +127 -0
  78. package/components/ui/popover.tsx +48 -0
  79. package/components/ui/progress.tsx +31 -0
  80. package/components/ui/radio-group.tsx +56 -0
  81. package/components/ui/rating.tsx +102 -0
  82. package/components/ui/resizable.tsx +405 -0
  83. package/components/ui/route-map.tsx +246 -0
  84. package/components/ui/scroll-area.tsx +58 -0
  85. package/components/ui/search.tsx +70 -0
  86. package/components/ui/select.tsx +176 -0
  87. package/components/ui/separator.tsx +28 -0
  88. package/components/ui/sheet.tsx +138 -0
  89. package/components/ui/sidebar.tsx +726 -0
  90. package/components/ui/simple-map.tsx +92 -0
  91. package/components/ui/skeleton.tsx +13 -0
  92. package/components/ui/slider.tsx +58 -0
  93. package/components/ui/sonner.tsx +77 -0
  94. package/components/ui/stats-card.tsx +84 -0
  95. package/components/ui/stepper.tsx +126 -0
  96. package/components/ui/switch.tsx +34 -0
  97. package/components/ui/table.tsx +116 -0
  98. package/components/ui/tabs.tsx +66 -0
  99. package/components/ui/textarea.tsx +26 -0
  100. package/components/ui/timeline.tsx +140 -0
  101. package/components/ui/toggle-group.tsx +71 -0
  102. package/components/ui/toggle.tsx +46 -0
  103. package/components/ui/tooltip.tsx +61 -0
  104. package/components/ui/tree-view.tsx +123 -0
  105. package/components/ui/use-mobile.ts +24 -0
  106. package/components/ui/utils.ts +6 -0
  107. package/components/ui/xertica-assistant.tsx +1420 -0
  108. package/contexts/ApiKeyContext.tsx +123 -0
  109. package/contexts/AssistenteContext.tsx +118 -0
  110. package/contexts/BrandColorsContext.tsx +551 -0
  111. package/contexts/LanguageContext.tsx +36 -0
  112. package/contexts/ThemeContext.tsx +85 -0
  113. package/dist/cli.js +20922 -0
  114. package/eslint.config.js +41 -0
  115. package/guidelines/Guidelines.md +61 -0
  116. package/hooks/useTheme.ts +4 -0
  117. package/imports/Podcast.tsx +389 -0
  118. package/imports/XerticaAi.tsx +46 -0
  119. package/imports/XerticaX.tsx +20 -0
  120. package/imports/svg-aueiaqngck.ts +11 -0
  121. package/imports/svg-v9krss1ozd.ts +16 -0
  122. package/imports/svg-vhrdofe3qe.ts +5 -0
  123. package/index.css +4448 -0
  124. package/index.html +14 -0
  125. package/main.tsx +10 -0
  126. package/package.json +119 -0
  127. package/postcss.config.js +6 -0
  128. package/routes.tsx +33 -0
  129. package/styles/globals.css +15 -0
  130. package/styles/xertica/app-overrides/chat.css +61 -0
  131. package/styles/xertica/app-overrides/scrollbar.css +33 -0
  132. package/styles/xertica/base.css +70 -0
  133. package/styles/xertica/integrations/google-maps.css +76 -0
  134. package/styles/xertica/integrations/sonner.css +73 -0
  135. package/styles/xertica/theme-map.css +88 -0
  136. package/styles/xertica/tokens.css +190 -0
  137. package/tsconfig.json +31 -0
  138. package/tsconfig.node.json +10 -0
  139. package/utils/gemini.ts +140 -0
  140. package/vite-env.d.ts +12 -0
  141. package/vite.config.ts +36 -0
@@ -0,0 +1,502 @@
1
+ import React, { useRef, useEffect, useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { Send, Plus, Paperclip, Mic, FileText, Radio, Search, X } from 'lucide-react';
4
+ import { Button } from './ui/button';
5
+ import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
6
+ import { toast } from 'sonner';
7
+
8
+ export type ActionType = 'document' | 'podcast' | 'search' | null;
9
+
10
+ interface ModernChatInputProps {
11
+ value: string;
12
+ onChange: (value: string) => void;
13
+ onSubmit: (action?: ActionType) => void;
14
+ placeholder?: string;
15
+ disabled?: boolean;
16
+ onFileUpload?: () => void;
17
+ onAudioUpload?: () => void;
18
+ onVoiceRecording?: (transcript: string) => void;
19
+ isFullPage?: boolean;
20
+ }
21
+
22
+ export function ModernChatInput({
23
+ value,
24
+ onChange,
25
+ onSubmit,
26
+ placeholder = "Envie uma mensagem para Xertica",
27
+ disabled = false,
28
+ onFileUpload,
29
+ onAudioUpload,
30
+ onVoiceRecording,
31
+ isFullPage = false
32
+ }: ModernChatInputProps) {
33
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
34
+ const [isFocused, setIsFocused] = useState(false);
35
+ const [textareaHeight, setTextareaHeight] = useState(20);
36
+ const [selectedAction, setSelectedAction] = useState<ActionType>(null);
37
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
38
+ const [isRecording, setIsRecording] = useState(false);
39
+ const [recordingTime, setRecordingTime] = useState(0);
40
+ const recordingIntervalRef = useRef<NodeJS.Timeout | null>(null);
41
+
42
+ const adjustHeight = () => {
43
+ const textarea = textareaRef.current;
44
+ if (textarea) {
45
+ // Reset to minimum height first
46
+ textarea.style.height = '20px';
47
+
48
+ // Calculate new height based on content
49
+ const scrollHeight = textarea.scrollHeight;
50
+ const newHeight = Math.min(Math.max(scrollHeight, 20), 100);
51
+
52
+ // Apply the new height smoothly
53
+ setTextareaHeight(newHeight);
54
+ textarea.style.height = newHeight + 'px';
55
+ }
56
+ };
57
+
58
+ // Initialize with proper height on mount
59
+ useEffect(() => {
60
+ const textarea = textareaRef.current;
61
+ if (textarea) {
62
+ // Force initial height without content check
63
+ textarea.style.height = '20px';
64
+ setTextareaHeight(20);
65
+
66
+ // If there's initial content, adjust immediately
67
+ if (value) {
68
+ setTimeout(adjustHeight, 0);
69
+ }
70
+ }
71
+ }, []); // Empty dependency array - only runs on mount
72
+
73
+ // Adjust height when value changes
74
+ useEffect(() => {
75
+ if (value === '') {
76
+ // Reset to minimum when empty
77
+ const textarea = textareaRef.current;
78
+ if (textarea) {
79
+ textarea.style.height = '20px';
80
+ setTextareaHeight(20);
81
+ }
82
+ } else {
83
+ // Adjust for content
84
+ const timeoutId = setTimeout(adjustHeight, 0);
85
+ return () => clearTimeout(timeoutId);
86
+ }
87
+ }, [value]);
88
+
89
+ const handleKeyDown = (e: React.KeyboardEvent) => {
90
+ if (e.key === 'Enter' && !e.shiftKey) {
91
+ e.preventDefault();
92
+ if (value.trim() && !disabled) {
93
+ onSubmit(selectedAction);
94
+ setSelectedAction(null);
95
+ }
96
+ }
97
+ };
98
+
99
+ const handleActionSelect = (action: ActionType) => {
100
+ setSelectedAction(action);
101
+ setIsPopoverOpen(false);
102
+ };
103
+
104
+ const handleRemoveAction = () => {
105
+ setSelectedAction(null);
106
+ };
107
+
108
+ const handleVoiceRecording = () => {
109
+ if (isRecording) {
110
+ // Parar gravação
111
+ if (recordingIntervalRef.current) {
112
+ clearInterval(recordingIntervalRef.current);
113
+ recordingIntervalRef.current = null;
114
+ }
115
+
116
+ // Simular transcrição do áudio
117
+ const transcricoes = [
118
+ "Como posso analisar os dados de vendas do último trimestre?",
119
+ "Me ajude a criar um relatório sobre o desempenho da equipe",
120
+ "Quais são as principais tendências do mercado atual?",
121
+ "Preciso de insights sobre a satisfação dos clientes",
122
+ "Como posso melhorar a produtividade da minha equipe?",
123
+ "Me mostre um resumo das métricas mais importantes",
124
+ "Crie um documento sobre estratégias de marketing digital",
125
+ "Analise os dados de engajamento das redes sociais"
126
+ ];
127
+
128
+ const transcricaoAleatoria = transcricoes[Math.floor(Math.random() * transcricoes.length)];
129
+
130
+ // Chamar o callback com a transcrição
131
+ if (onVoiceRecording) {
132
+ onVoiceRecording(transcricaoAleatoria);
133
+ }
134
+
135
+ setIsRecording(false);
136
+ setRecordingTime(0);
137
+ } else {
138
+ // Iniciar gravação
139
+ setIsRecording(true);
140
+ setRecordingTime(0);
141
+
142
+ toast.info('Gravação iniciada', {
143
+ description: 'Fale sua pergunta ou comando. A gravação parará automaticamente em 60s.',
144
+ duration: 3000,
145
+ });
146
+
147
+ recordingIntervalRef.current = setInterval(() => {
148
+ setRecordingTime(prev => prev + 1);
149
+ }, 1000);
150
+ }
151
+ };
152
+
153
+ // Parar gravação automaticamente após 60 segundos
154
+ useEffect(() => {
155
+ if (recordingTime >= 60 && isRecording) {
156
+ handleVoiceRecording();
157
+ }
158
+ }, [recordingTime, isRecording]);
159
+
160
+ // Limpar intervalo ao desmontar
161
+ useEffect(() => {
162
+ return () => {
163
+ if (recordingIntervalRef.current) {
164
+ clearInterval(recordingIntervalRef.current);
165
+ }
166
+ };
167
+ }, []);
168
+
169
+ const getActionInfo = (action: ActionType) => {
170
+ switch (action) {
171
+ case 'document':
172
+ return { label: 'Criar documento', icon: FileText, color: 'bg-[var(--chart-4)]' };
173
+ case 'podcast':
174
+ return { label: 'Gerar podcast', icon: Radio, color: 'bg-[var(--chart-1)]' };
175
+ case 'search':
176
+ return { label: 'Pesquisar', icon: Search, color: 'bg-[var(--chart-2)]' };
177
+ default:
178
+ return null;
179
+ }
180
+ };
181
+
182
+ const handleFocus = () => setIsFocused(true);
183
+ const handleBlur = () => setIsFocused(false);
184
+
185
+ const hasContent = value.trim().length > 0;
186
+
187
+ return (
188
+ <div className={`p-4 ${isFullPage ? 'mx-auto w-full max-w-6xl' : ''}`}>
189
+ {/* Recording Indicator */}
190
+ <AnimatePresence>
191
+ {isRecording && (
192
+ <motion.div
193
+ initial={{ opacity: 0, y: 10 }}
194
+ animate={{ opacity: 1, y: 0 }}
195
+ exit={{ opacity: 0, y: 10 }}
196
+ className="mb-3 p-4 bg-destructive/10 border border-destructive/30 rounded-[var(--radius-card)]"
197
+ >
198
+ <div className="flex items-center justify-between">
199
+ <div className="flex items-center gap-3">
200
+ {/* Pulsing Dot */}
201
+ <motion.div
202
+ animate={{
203
+ scale: [1, 1.2, 1],
204
+ opacity: [1, 0.7, 1],
205
+ }}
206
+ transition={{
207
+ duration: 1.5,
208
+ repeat: Infinity,
209
+ ease: "easeInOut"
210
+ }}
211
+ className="w-3 h-3 bg-destructive rounded-full"
212
+ />
213
+
214
+ {/* Audio Waves Animation */}
215
+ <div className="flex items-center gap-1">
216
+ {[0, 1, 2, 3, 4].map((i) => (
217
+ <motion.div
218
+ key={i}
219
+ animate={{
220
+ height: ['8px', '20px', '8px'],
221
+ }}
222
+ transition={{
223
+ duration: 0.8,
224
+ repeat: Infinity,
225
+ ease: "easeInOut",
226
+ delay: i * 0.1,
227
+ }}
228
+ className="w-1 bg-destructive rounded-full h-2"
229
+ />
230
+ ))}
231
+ </div>
232
+
233
+ <span className="text-sm font-medium text-destructive">
234
+ Gravando áudio
235
+ </span>
236
+ </div>
237
+
238
+ <div className="flex items-center gap-3">
239
+ <div className="text-sm text-destructive font-mono tabular-nums">
240
+ {Math.floor(recordingTime / 60)}:{(recordingTime % 60).toString().padStart(2, '0')}
241
+ </div>
242
+ <Button
243
+ onClick={handleVoiceRecording}
244
+ size="sm"
245
+ variant="outline"
246
+ className="h-7 px-3 text-xs border-destructive/30 text-destructive hover:bg-destructive/10 rounded-[var(--radius-button)]"
247
+ >
248
+ Parar gravação
249
+ </Button>
250
+ </div>
251
+ </div>
252
+ </motion.div>
253
+ )}
254
+ </AnimatePresence>
255
+
256
+ {/* Modern Input Container */}
257
+ <motion.div
258
+ className={`relative bg-card rounded-[var(--radius)] border shadow-sm transition-all duration-300 ${
259
+ isFocused || hasContent
260
+ ? 'border-primary shadow-lg shadow-primary/10'
261
+ : 'border-border'
262
+ }`}
263
+ initial={false}
264
+ animate={{
265
+ scale: isFocused ? 1.01 : 1,
266
+ }}
267
+ transition={{ duration: 0.2, ease: "easeOut" }}
268
+ >
269
+ <div className="px-3 py-2 pt-[14px] pr-[10px] pb-[8px] pl-[10px] rounded-[var(--radius)]">
270
+ {/* Action Chip */}
271
+ <AnimatePresence>
272
+ {selectedAction && (
273
+ <motion.div
274
+ initial={{ opacity: 0, y: -10, height: 0 }}
275
+ animate={{ opacity: 1, y: 0, height: 'auto' }}
276
+ exit={{ opacity: 0, y: -10, height: 0 }}
277
+ transition={{ duration: 0.2 }}
278
+ className="mb-2"
279
+ >
280
+ {(() => {
281
+ const actionInfo = getActionInfo(selectedAction);
282
+ if (!actionInfo) return null;
283
+ const Icon = actionInfo.icon;
284
+ return (
285
+ <div className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-full ${actionInfo.color} text-white text-xs font-medium shadow-sm`}>
286
+ <Icon className="w-3.5 h-3.5" />
287
+ <span>{actionInfo.label}</span>
288
+ <button
289
+ onClick={handleRemoveAction}
290
+ className="ml-1 hover:bg-white/20 rounded-full p-0.5 transition-colors"
291
+ >
292
+ <X className="w-3 h-3" />
293
+ </button>
294
+ </div>
295
+ );
296
+ })()}
297
+ </motion.div>
298
+ )}
299
+ </AnimatePresence>
300
+
301
+ {/* Text Input - Full Width */}
302
+ <div className="mb-2">
303
+ <textarea
304
+ ref={textareaRef}
305
+ value={value}
306
+ onChange={(e) => onChange(e.target.value)}
307
+ onKeyDown={handleKeyDown}
308
+ onFocus={handleFocus}
309
+ onBlur={handleBlur}
310
+ placeholder={placeholder}
311
+ disabled={disabled}
312
+ className="w-full bg-transparent border-0 outline-none resize-none text-foreground placeholder-muted-foreground text-sm leading-5 min-h-[20px] max-h-[100px] overflow-y-auto scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent"
313
+ style={{
314
+ height: `${textareaHeight}px`,
315
+ minHeight: '20px',
316
+ maxHeight: '100px',
317
+ }}
318
+ rows={1}
319
+ />
320
+ </div>
321
+
322
+ {/* Actions Row - Bottom */}
323
+ <div className="flex items-center justify-between">
324
+ {/* Left Actions */}
325
+ <div className="flex items-center gap-1">
326
+ <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
327
+ <PopoverTrigger asChild>
328
+ <motion.div
329
+ whileHover={{ scale: 1.05 }}
330
+ whileTap={{ scale: 0.95 }}
331
+ >
332
+ <Button
333
+ variant="ghost"
334
+ size="sm"
335
+ className="h-8 w-8 p-0 rounded-full text-muted-foreground hover:bg-accent hover:text-foreground transition-all duration-200"
336
+ >
337
+ <Plus className="w-4 h-4" />
338
+ </Button>
339
+ </motion.div>
340
+ </PopoverTrigger>
341
+ <PopoverContent
342
+ className="w-56 p-2"
343
+ align="start"
344
+ side="top"
345
+ >
346
+ <div className="space-y-1">
347
+ <button
348
+ onClick={() => handleActionSelect('document')}
349
+ className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-accent transition-colors text-left group"
350
+ >
351
+ <div className="w-8 h-8 rounded-full bg-[var(--chart-4)]/20 flex items-center justify-center group-hover:bg-[var(--chart-4)] transition-colors">
352
+ <FileText className="w-4 h-4 text-[var(--chart-4)] group-hover:text-white" />
353
+ </div>
354
+ <div className="flex-1">
355
+ <div className="text-sm font-medium text-foreground">
356
+ Criar documento
357
+ </div>
358
+ <div className="text-xs text-muted-foreground">
359
+ Gerar um documento completo
360
+ </div>
361
+ </div>
362
+ </button>
363
+
364
+ <button
365
+ onClick={() => handleActionSelect('podcast')}
366
+ className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-accent transition-colors text-left group"
367
+ >
368
+ <div className="w-8 h-8 rounded-full bg-[var(--chart-1)]/20 flex items-center justify-center group-hover:bg-[var(--chart-1)] transition-colors">
369
+ <Radio className="w-4 h-4 text-[var(--chart-1)] group-hover:text-white" />
370
+ </div>
371
+ <div className="flex-1">
372
+ <div className="text-sm font-medium text-foreground">
373
+ Gerar podcast
374
+ </div>
375
+ <div className="text-xs text-muted-foreground">
376
+ Criar um podcast em áudio
377
+ </div>
378
+ </div>
379
+ </button>
380
+
381
+ <button
382
+ onClick={() => handleActionSelect('search')}
383
+ className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-accent transition-colors text-left group"
384
+ >
385
+ <div className="w-8 h-8 rounded-full bg-[var(--chart-2)]/20 flex items-center justify-center group-hover:bg-[var(--chart-2)] transition-colors">
386
+ <Search className="w-4 h-4 text-[var(--chart-2)] group-hover:text-white" />
387
+ </div>
388
+ <div className="flex-1">
389
+ <div className="text-sm font-medium text-foreground">
390
+ Pesquisar
391
+ </div>
392
+ <div className="text-xs text-muted-foreground">
393
+ Buscar informações relevantes
394
+ </div>
395
+ </div>
396
+ </button>
397
+ </div>
398
+ </PopoverContent>
399
+ </Popover>
400
+
401
+ <motion.div
402
+ whileHover={{ scale: 1.05 }}
403
+ whileTap={{ scale: 0.95 }}
404
+ >
405
+ <Button
406
+ variant="ghost"
407
+ size="sm"
408
+ onClick={onFileUpload}
409
+ className="h-8 w-8 p-0 rounded-full text-muted-foreground hover:bg-accent hover:text-foreground transition-all duration-200"
410
+ >
411
+ <Paperclip className="w-4 h-4" />
412
+ </Button>
413
+ </motion.div>
414
+ </div>
415
+
416
+ {/* Right Actions */}
417
+ <div className="flex items-center gap-1">
418
+ {/* Voice Recording Button */}
419
+ <motion.div
420
+ whileHover={{ scale: 1.05 }}
421
+ whileTap={{ scale: 0.95 }}
422
+ >
423
+ <Button
424
+ variant="ghost"
425
+ size="sm"
426
+ onClick={handleVoiceRecording}
427
+ className={`h-8 w-8 p-0 rounded-full transition-all duration-200 ${
428
+ isRecording
429
+ ? 'bg-destructive hover:bg-destructive/90 text-white animate-pulse'
430
+ : 'text-muted-foreground hover:bg-accent hover:text-foreground'
431
+ }`}
432
+ >
433
+ <Mic className="w-4 h-4" />
434
+ </Button>
435
+ </motion.div>
436
+
437
+ <AnimatePresence mode="wait">
438
+ <motion.div
439
+ key={hasContent ? 'active' : 'inactive'}
440
+ initial={{ scale: 0.8, opacity: 0 }}
441
+ animate={{ scale: 1, opacity: 1 }}
442
+ exit={{ scale: 0.8, opacity: 0 }}
443
+ transition={{ duration: 0.2, ease: "easeInOut" }}
444
+ whileHover={hasContent ? { scale: 1.05 } : {}}
445
+ whileTap={hasContent ? { scale: 0.95 } : {}}
446
+ >
447
+ <Button
448
+ onClick={() => {
449
+ onSubmit(selectedAction);
450
+ setSelectedAction(null);
451
+ }}
452
+ size="sm"
453
+ disabled={!hasContent || disabled}
454
+ className={`h-8 w-8 p-0 rounded-full transition-all duration-300 ${
455
+ hasContent && !disabled
456
+ ? 'bg-primary hover:bg-primary/90 text-primary-foreground shadow-lg shadow-primary/30 hover:shadow-primary/40'
457
+ : 'bg-muted text-muted-foreground cursor-not-allowed'
458
+ }`}
459
+ >
460
+ <motion.div
461
+ animate={{
462
+ rotate: hasContent ? 0 : -45,
463
+ }}
464
+ transition={{ duration: 0.2 }}
465
+ >
466
+ <Send className="w-4 h-4" />
467
+ </motion.div>
468
+ </Button>
469
+ </motion.div>
470
+ </AnimatePresence>
471
+ </div>
472
+ </div>
473
+ </div>
474
+
475
+ {/* Subtle glow effect when focused */}
476
+ <AnimatePresence>
477
+ {isFocused && (
478
+ <motion.div
479
+ className="absolute inset-0 rounded-[var(--radius)] bg-gradient-to-r from-primary/5 via-transparent to-primary/5 pointer-events-none"
480
+ initial={{ opacity: 0 }}
481
+ animate={{ opacity: 1 }}
482
+ exit={{ opacity: 0 }}
483
+ transition={{ duration: 0.3 }}
484
+ />
485
+ )}
486
+ </AnimatePresence>
487
+ </motion.div>
488
+
489
+ {/* Footer Info */}
490
+ <motion.div
491
+ className="mt-2 text-center"
492
+ initial={{ opacity: 0, y: 5 }}
493
+ animate={{ opacity: 1, y: 0 }}
494
+ transition={{ delay: 0.1 }}
495
+ >
496
+ <p className="text-xs text-muted-foreground">
497
+ O assistente Xertica pode cometer erros. Considere verificar informações importantes.
498
+ </p>
499
+ </motion.div>
500
+ </div>
501
+ );
502
+ }