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.
- package/App.tsx +182 -0
- package/README.md +330 -0
- package/assets/xertica-logo.svg +38 -0
- package/assets/xertica-x-logo.svg +21 -0
- package/bin/cli.ts +193 -0
- package/components/AssistenteXertica.tsx +2003 -0
- package/components/AudioPlayer.tsx +203 -0
- package/components/CodeBlock.tsx +242 -0
- package/components/DocumentEditor.tsx +504 -0
- package/components/ForgotPasswordPage.tsx +170 -0
- package/components/FormattedDocument.tsx +87 -0
- package/components/HomeContent.tsx +123 -0
- package/components/HomePage.tsx +70 -0
- package/components/LanguageSelector.tsx +54 -0
- package/components/LoginPage.tsx +199 -0
- package/components/MarkdownMessage.tsx +62 -0
- package/components/ModernChatInput.tsx +502 -0
- package/components/PodcastPlayer.tsx +409 -0
- package/components/ResetPasswordPage.tsx +234 -0
- package/components/Sidebar.tsx +489 -0
- package/components/TemplateContent.tsx +629 -0
- package/components/TemplatePage.tsx +70 -0
- package/components/ThemeToggle.tsx +65 -0
- package/components/VerifyEmailPage.tsx +187 -0
- package/components/XerticaLogo.tsx +69 -0
- package/components/XerticaOrbe.tsx +1339 -0
- package/components/XerticaXLogo.tsx +53 -0
- package/components/examples/DrawingMapExample.tsx +530 -0
- package/components/examples/FilterableMapExample.tsx +380 -0
- package/components/examples/LocationPickerExample.tsx +330 -0
- package/components/examples/MapExamples.tsx +280 -0
- package/components/examples/MapShowcase.tsx +446 -0
- package/components/examples/RouteMapExamples.tsx +329 -0
- package/components/examples/SimpleFilterableMap.tsx +192 -0
- package/components/examples/index.ts +52 -0
- package/components/figma/ImageWithFallback.tsx +27 -0
- package/components/index.ts +44 -0
- package/components/media/AudioPlayer.tsx +278 -0
- package/components/media/FloatingMediaWrapper.tsx +166 -0
- package/components/media/VideoPlayer.tsx +285 -0
- package/components/ui/accordion.tsx +66 -0
- package/components/ui/alert-dialog.tsx +159 -0
- package/components/ui/alert.tsx +91 -0
- package/components/ui/aspect-ratio.tsx +11 -0
- package/components/ui/avatar.tsx +65 -0
- package/components/ui/badge.tsx +55 -0
- package/components/ui/breadcrumb.tsx +109 -0
- package/components/ui/button.tsx +78 -0
- package/components/ui/calendar.tsx +235 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/carousel.tsx +241 -0
- package/components/ui/chart.tsx +353 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/collapsible.tsx +33 -0
- package/components/ui/command.tsx +177 -0
- package/components/ui/context-menu.tsx +252 -0
- package/components/ui/dialog.tsx +138 -0
- package/components/ui/drawer.tsx +134 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/empty.tsx +90 -0
- package/components/ui/file-upload.tsx +152 -0
- package/components/ui/form.tsx +195 -0
- package/components/ui/google-maps-loader.tsx +379 -0
- package/components/ui/hover-card.tsx +44 -0
- package/components/ui/index.ts +242 -0
- package/components/ui/input-otp.tsx +77 -0
- package/components/ui/input.tsx +38 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/map-config.ts +12 -0
- package/components/ui/map-layers.tsx +129 -0
- package/components/ui/map.exports.ts +31 -0
- package/components/ui/map.tsx +412 -0
- package/components/ui/menubar.tsx +276 -0
- package/components/ui/navigation-menu.tsx +162 -0
- package/components/ui/notification-badge.tsx +61 -0
- package/components/ui/page-header.tsx +229 -0
- package/components/ui/pagination.tsx +127 -0
- package/components/ui/popover.tsx +48 -0
- package/components/ui/progress.tsx +31 -0
- package/components/ui/radio-group.tsx +56 -0
- package/components/ui/rating.tsx +102 -0
- package/components/ui/resizable.tsx +405 -0
- package/components/ui/route-map.tsx +246 -0
- package/components/ui/scroll-area.tsx +58 -0
- package/components/ui/search.tsx +70 -0
- package/components/ui/select.tsx +176 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/sheet.tsx +138 -0
- package/components/ui/sidebar.tsx +726 -0
- package/components/ui/simple-map.tsx +92 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/slider.tsx +58 -0
- package/components/ui/sonner.tsx +77 -0
- package/components/ui/stats-card.tsx +84 -0
- package/components/ui/stepper.tsx +126 -0
- package/components/ui/switch.tsx +34 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +66 -0
- package/components/ui/textarea.tsx +26 -0
- package/components/ui/timeline.tsx +140 -0
- package/components/ui/toggle-group.tsx +71 -0
- package/components/ui/toggle.tsx +46 -0
- package/components/ui/tooltip.tsx +61 -0
- package/components/ui/tree-view.tsx +123 -0
- package/components/ui/use-mobile.ts +24 -0
- package/components/ui/utils.ts +6 -0
- package/components/ui/xertica-assistant.tsx +1420 -0
- package/contexts/ApiKeyContext.tsx +123 -0
- package/contexts/AssistenteContext.tsx +118 -0
- package/contexts/BrandColorsContext.tsx +551 -0
- package/contexts/LanguageContext.tsx +36 -0
- package/contexts/ThemeContext.tsx +85 -0
- package/dist/cli.js +20922 -0
- package/eslint.config.js +41 -0
- package/guidelines/Guidelines.md +61 -0
- package/hooks/useTheme.ts +4 -0
- package/imports/Podcast.tsx +389 -0
- package/imports/XerticaAi.tsx +46 -0
- package/imports/XerticaX.tsx +20 -0
- package/imports/svg-aueiaqngck.ts +11 -0
- package/imports/svg-v9krss1ozd.ts +16 -0
- package/imports/svg-vhrdofe3qe.ts +5 -0
- package/index.css +4448 -0
- package/index.html +14 -0
- package/main.tsx +10 -0
- package/package.json +119 -0
- package/postcss.config.js +6 -0
- package/routes.tsx +33 -0
- package/styles/globals.css +15 -0
- package/styles/xertica/app-overrides/chat.css +61 -0
- package/styles/xertica/app-overrides/scrollbar.css +33 -0
- package/styles/xertica/base.css +70 -0
- package/styles/xertica/integrations/google-maps.css +76 -0
- package/styles/xertica/integrations/sonner.css +73 -0
- package/styles/xertica/theme-map.css +88 -0
- package/styles/xertica/tokens.css +190 -0
- package/tsconfig.json +31 -0
- package/tsconfig.node.json +10 -0
- package/utils/gemini.ts +140 -0
- package/vite-env.d.ts +12 -0
- 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
|
+
}
|