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,1420 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import {
4
+ MessageSquare,
5
+ Heart,
6
+ History,
7
+ MoreHorizontal,
8
+ X,
9
+ ChevronLeft,
10
+ ChevronRight,
11
+ PanelRight,
12
+ Plus,
13
+ Copy,
14
+ Check,
15
+ FileText,
16
+ Music,
17
+ Image as ImageIcon,
18
+ Radio,
19
+ Loader2,
20
+ Edit,
21
+ Download,
22
+ Search,
23
+ Folder,
24
+ Users,
25
+ ExternalLink,
26
+ Star,
27
+ Clock,
28
+ FolderOpen,
29
+ Filter,
30
+ Mail,
31
+ BarChart3,
32
+ Bookmark,
33
+ Maximize2,
34
+ AlertCircle
35
+ } from 'lucide-react';
36
+ import { Button } from './button';
37
+ import { ScrollArea } from './scroll-area';
38
+ import { Separator } from './separator';
39
+ import { MarkdownMessage } from '../MarkdownMessage';
40
+ import { FormattedDocument } from '../FormattedDocument';
41
+ import { ModernChatInput } from '../ModernChatInput';
42
+ import type { ActionType } from '../ModernChatInput';
43
+ import { XerticaOrbe } from '../XerticaOrbe';
44
+ import { Tooltip, TooltipTrigger } from './tooltip';
45
+ import * as TooltipPrimitive from '@radix-ui/react-tooltip';
46
+ import { cn } from './utils';
47
+
48
+ // Tooltip customizado estilo sidebar (branco)
49
+ function AssistantTooltipContent({
50
+ className,
51
+ sideOffset = 0,
52
+ children,
53
+ ...props
54
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
55
+ return (
56
+ <TooltipPrimitive.Portal>
57
+ <TooltipPrimitive.Content
58
+ data-slot="tooltip-content"
59
+ sideOffset={sideOffset}
60
+ className={cn(
61
+ "bg-popover text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
62
+ className,
63
+ )}
64
+ {...props}
65
+ >
66
+ {children}
67
+ <TooltipPrimitive.Arrow
68
+ className="fill-popover z-50 drop-shadow-sm"
69
+ width={8}
70
+ height={4}
71
+ />
72
+ </TooltipPrimitive.Content>
73
+ </TooltipPrimitive.Portal>
74
+ );
75
+ }
76
+
77
+ // ============================================================================
78
+ // TypeScript Interfaces & Types
79
+ // ============================================================================
80
+
81
+ /**
82
+ * Tipos de mensagem suportados pelo assistente
83
+ */
84
+ export type MessageType = 'user' | 'assistant';
85
+
86
+ /**
87
+ * Tipos de anexo que podem ser incluídos em mensagens
88
+ */
89
+ export type AttachmentType = 'file' | 'audio' | 'image' | 'document' | 'podcast' | 'search';
90
+
91
+ /**
92
+ * Tipo de busca que pode ser realizada
93
+ */
94
+ export type SearchResultType = 'document' | 'project' | 'conversation' | 'file' | 'contact';
95
+
96
+ /**
97
+ * Interface para resultados de busca
98
+ */
99
+ export interface SearchResult {
100
+ id: string;
101
+ type: SearchResultType;
102
+ title: string;
103
+ description: string;
104
+ path: string;
105
+ relevance: number;
106
+ lastModified?: string;
107
+ }
108
+
109
+ /**
110
+ * Interface para fontes de busca
111
+ */
112
+ export interface SearchSource {
113
+ name: string;
114
+ count: number;
115
+ }
116
+
117
+ /**
118
+ * Interface para comandos de busca
119
+ */
120
+ export interface SearchCommand {
121
+ id: string;
122
+ icon: string;
123
+ label: string;
124
+ description: string;
125
+ }
126
+
127
+ /**
128
+ * Interface para uma mensagem individual
129
+ */
130
+ export interface Message {
131
+ id: string;
132
+ type: MessageType;
133
+ content: string;
134
+ timestamp: Date;
135
+ isFavorite?: boolean;
136
+ attachmentType?: AttachmentType;
137
+ attachmentName?: string;
138
+ documentContent?: string;
139
+ documentTitle?: string;
140
+ audioUrl?: string;
141
+ searchResults?: SearchResult[];
142
+ searchSources?: SearchSource[];
143
+ searchCommands?: SearchCommand[];
144
+ }
145
+
146
+ /**
147
+ * Interface para uma conversa salva
148
+ */
149
+ export interface Conversation {
150
+ id: string;
151
+ titulo: string;
152
+ ultimaMensagem?: string;
153
+ timestamp: string;
154
+ favorita: boolean;
155
+ mensagens: Message[];
156
+ }
157
+
158
+ /**
159
+ * Interface para sugestões de chat
160
+ */
161
+ export interface Suggestion {
162
+ id: string;
163
+ texto: string;
164
+ }
165
+
166
+ /**
167
+ * Modo de exibição do assistente
168
+ */
169
+ export type AssistantMode = 'collapsed' | 'expanded' | 'fullPage';
170
+
171
+ /**
172
+ * Abas disponíveis no assistente
173
+ */
174
+ export type AssistantTab = 'chat' | 'historico' | 'favoritos';
175
+
176
+ // ============================================================================
177
+ // Props do Componente
178
+ // ============================================================================
179
+
180
+ export interface XerticaAssistantProps {
181
+ /**
182
+ * Modo de exibição do assistente
183
+ * @default 'expanded'
184
+ */
185
+ mode?: AssistantMode;
186
+
187
+ /**
188
+ * Se o assistente está expandido (apenas para mode='expanded')
189
+ * @default true
190
+ */
191
+ isExpanded?: boolean;
192
+
193
+ /**
194
+ * Callback chamado quando o assistente é expandido/colapsado
195
+ */
196
+ onToggle?: () => void;
197
+
198
+ /**
199
+ * Aba selecionada inicialmente
200
+ * @default 'chat'
201
+ */
202
+ defaultTab?: AssistantTab;
203
+
204
+ /**
205
+ * Se deve mostrar banner de aviso de API key
206
+ * @default true
207
+ */
208
+ showApiWarning?: boolean;
209
+
210
+ /**
211
+ * Chave da API do Gemini (se disponível)
212
+ */
213
+ apiKey?: string;
214
+
215
+ /**
216
+ * Callback chamado ao navegar para página de configurações
217
+ */
218
+ onNavigateSettings?: () => void;
219
+
220
+ /**
221
+ * Callback chamado ao navegar para página full do assistente
222
+ */
223
+ onNavigateFullPage?: () => void;
224
+
225
+ /**
226
+ * Nome do usuário logado
227
+ * @default 'Usuário'
228
+ */
229
+ userName?: string;
230
+
231
+ /**
232
+ * Mensagens iniciais (para carregar conversa existente)
233
+ */
234
+ initialMessages?: Message[];
235
+
236
+ /**
237
+ * Conversas salvas
238
+ */
239
+ savedConversations?: Conversation[];
240
+
241
+ /**
242
+ * Sugestões de mensagens
243
+ */
244
+ suggestions?: Suggestion[];
245
+
246
+ /**
247
+ * Callback chamado quando uma mensagem é enviada
248
+ */
249
+ onSendMessage?: (message: string) => void;
250
+
251
+ /**
252
+ * Callback chamado quando um arquivo é anexado
253
+ */
254
+ onFileAttach?: (file: File) => void;
255
+
256
+ /**
257
+ * Se está processando mensagem (mostra typing indicator)
258
+ */
259
+ isProcessing?: boolean;
260
+
261
+ /**
262
+ * Largura customizada (apenas para mode='expanded')
263
+ */
264
+ width?: string;
265
+
266
+ /**
267
+ * Altura customizada (apenas para mode='fullPage')
268
+ */
269
+ height?: string;
270
+
271
+ /**
272
+ * Classes CSS adicionais
273
+ */
274
+ className?: string;
275
+ mobileFloating?: boolean;
276
+ }
277
+
278
+ // ============================================================================
279
+ // Componente Principal
280
+ // ============================================================================
281
+
282
+ /**
283
+ * XerticaAssistant - Assistente de IA completo com chat, histórico e favoritos
284
+ *
285
+ * Suporta três modos de exibição:
286
+ * - collapsed: Apenas ícones na lateral
287
+ * - expanded: Painel lateral expansível
288
+ * - fullPage: Página dedicada completa
289
+ *
290
+ * @example
291
+ * ```tsx
292
+ * // Modo expandido (padrão)
293
+ * <XerticaAssistant
294
+ * mode="expanded"
295
+ * userName="João Silva"
296
+ * onSendMessage={(msg) => console.log(msg)}
297
+ * />
298
+ *
299
+ * // Modo página completa
300
+ * <XerticaAssistant
301
+ * mode="fullPage"
302
+ * height="100vh"
303
+ * />
304
+ *
305
+ * // Modo colapsado
306
+ * <XerticaAssistant
307
+ * mode="expanded"
308
+ * isExpanded={false}
309
+ * onToggle={() => setExpanded(!expanded)}
310
+ * />
311
+ * ```
312
+ */
313
+ export function XerticaAssistant({
314
+ mode = 'expanded',
315
+ isExpanded: controlledIsExpanded,
316
+ onToggle,
317
+ defaultTab = 'chat',
318
+ showApiWarning = true,
319
+ apiKey,
320
+ onNavigateSettings,
321
+ onNavigateFullPage,
322
+ userName = 'Usuário',
323
+ initialMessages = [],
324
+ savedConversations = [],
325
+ suggestions: propSuggestions,
326
+ onSendMessage,
327
+ onFileAttach,
328
+ isProcessing = false,
329
+ width,
330
+ height,
331
+ className = '',
332
+ mobileFloating = false,
333
+ }: XerticaAssistantProps) {
334
+ // ============================================================================
335
+ // State Management
336
+ // ============================================================================
337
+
338
+ const isFullPage = mode === 'fullPage';
339
+ const [internalIsExpanded, setInternalIsExpanded] = useState(controlledIsExpanded ?? true);
340
+ const isExpanded = controlledIsExpanded ?? internalIsExpanded;
341
+
342
+ const [abaSelecionada, setAbaSelecionada] = useState<AssistantTab>(defaultTab);
343
+ const [mensagens, setMensagens] = useState<Message[]>(initialMessages);
344
+ const [mensagem, setMensagem] = useState('');
345
+ const [conversas, setConversas] = useState<Conversation[]>(savedConversations);
346
+ const [conversaAtual, setConversaAtual] = useState<string | null>(null);
347
+ const [copiedId, setCopiedId] = useState<string | null>(null);
348
+ const [generatingPodcastId, setGeneratingPodcastId] = useState<string | null>(null);
349
+ const [executingCommand, setExecutingCommand] = useState<string | null>(null);
350
+ const [savedSearches, setSavedSearches] = useState<string[]>([]);
351
+ const [editingDocument, setEditingDocument] = useState<{
352
+ content: string;
353
+ title: string;
354
+ } | null>(null);
355
+
356
+ // ============================================================================
357
+ // Refs
358
+ // ============================================================================
359
+
360
+ const messagesEndRef = useRef<HTMLDivElement>(null);
361
+ const fileInputRef = useRef<HTMLInputElement>(null);
362
+ const audioInputRef = useRef<HTMLInputElement>(null);
363
+
364
+ // ============================================================================
365
+ // Sugestões padrão
366
+ // ============================================================================
367
+
368
+ const defaultSuggestions: Suggestion[] = [
369
+ { id: '1', texto: 'Me ajude a criar um documento profissional' },
370
+ { id: '2', texto: 'Buscar nos meus arquivos por "relatório"' },
371
+ { id: '3', texto: 'Resuma as conversas importantes desta semana' },
372
+ { id: '4', texto: 'Crie um podcast sobre o último projeto' },
373
+ ];
374
+
375
+ const sugestoes = propSuggestions ?? defaultSuggestions;
376
+
377
+ // ============================================================================
378
+ // API Key Validation
379
+ // ============================================================================
380
+
381
+ const isApiKeyValid = Boolean(apiKey && apiKey.length > 20);
382
+
383
+ // ============================================================================
384
+ // Effects
385
+ // ============================================================================
386
+
387
+ // Auto-scroll ao adicionar mensagens
388
+ useEffect(() => {
389
+ if (messagesEndRef.current && abaSelecionada === 'chat') {
390
+ messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
391
+ }
392
+ }, [mensagens, abaSelecionada]);
393
+
394
+ // Sincronizar mensagens iniciais
395
+ useEffect(() => {
396
+ if (initialMessages.length > 0) {
397
+ setMensagens(initialMessages);
398
+ }
399
+ }, [initialMessages]);
400
+
401
+ // ============================================================================
402
+ // Handlers
403
+ // ============================================================================
404
+
405
+ const handleToggle = () => {
406
+ if (onToggle) {
407
+ onToggle();
408
+ } else {
409
+ setInternalIsExpanded(!internalIsExpanded);
410
+ }
411
+ };
412
+
413
+ const handleExpandWithTab = (tab: AssistantTab) => {
414
+ setAbaSelecionada(tab);
415
+ if (onToggle) {
416
+ onToggle();
417
+ } else {
418
+ setInternalIsExpanded(true);
419
+ }
420
+ };
421
+
422
+ const handleEnviarMensagem = async () => {
423
+ if (!mensagem.trim() || isProcessing) return;
424
+
425
+ const novaMensagem: Message = {
426
+ id: `msg-${Date.now()}`,
427
+ type: 'user',
428
+ content: mensagem,
429
+ timestamp: new Date(),
430
+ isFavorite: false,
431
+ };
432
+
433
+ setMensagens(prev => [...prev, novaMensagem]);
434
+
435
+ if (onSendMessage) {
436
+ onSendMessage(mensagem);
437
+ }
438
+
439
+ setMensagem('');
440
+ };
441
+
442
+ const handleToggleFavorite = (messageId: string) => {
443
+ setMensagens(prev =>
444
+ prev.map(msg =>
445
+ msg.id === messageId ? { ...msg, isFavorite: !msg.isFavorite } : msg
446
+ )
447
+ );
448
+ };
449
+
450
+ const handleCopyMessage = async (content: string, messageId: string) => {
451
+ try {
452
+ // Try modern Clipboard API first
453
+ if (navigator.clipboard && navigator.clipboard.writeText) {
454
+ try {
455
+ await navigator.clipboard.writeText(content);
456
+ setCopiedId(messageId);
457
+ setTimeout(() => setCopiedId(null), 2000);
458
+ } catch (clipboardError: any) {
459
+ // If clipboard API fails due to permissions, fall back to execCommand
460
+ if (clipboardError.name === 'NotAllowedError' || clipboardError.message.includes('permissions policy')) {
461
+ throw new Error('Clipboard permission denied, falling back');
462
+ }
463
+ throw clipboardError;
464
+ }
465
+ } else {
466
+ throw new Error('Clipboard API not available');
467
+ }
468
+ } catch (err) {
469
+ // Fallback for browsers that don't support Clipboard API or when permissions are denied
470
+ try {
471
+ const textArea = document.createElement('textarea');
472
+ textArea.value = content;
473
+ textArea.style.position = 'fixed';
474
+ textArea.style.left = '-999999px';
475
+ textArea.style.top = '-999999px';
476
+ document.body.appendChild(textArea);
477
+ textArea.focus();
478
+ textArea.select();
479
+
480
+ const successful = document.execCommand('copy');
481
+ document.body.removeChild(textArea);
482
+
483
+ if (successful) {
484
+ setCopiedId(messageId);
485
+ setTimeout(() => setCopiedId(null), 2000);
486
+ } else {
487
+ console.error('Falha ao copiar: execCommand returned false');
488
+ }
489
+ } catch (fallbackErr) {
490
+ console.error('Falha ao copiar:', fallbackErr);
491
+ }
492
+ }
493
+ };
494
+
495
+ const handleGeneratePodcast = async (messageId: string, content: string) => {
496
+ setGeneratingPodcastId(messageId);
497
+
498
+ // Simulação de geração de podcast
499
+ setTimeout(() => {
500
+ setMensagens(prev =>
501
+ prev.map(msg =>
502
+ msg.id === messageId
503
+ ? {
504
+ ...msg,
505
+ attachmentType: 'podcast',
506
+ attachmentName: 'Podcast - ' + content.substring(0, 30) + '...',
507
+ audioUrl: 'data:audio/mpeg;base64,//uQx...' // Mock
508
+ }
509
+ : msg
510
+ )
511
+ );
512
+ setGeneratingPodcastId(null);
513
+ }, 2000);
514
+ };
515
+
516
+ const handleDownloadDocument = (content: string, fileName: string) => {
517
+ const blob = new Blob([content], { type: 'text/markdown' });
518
+ const url = URL.createObjectURL(blob);
519
+ const a = document.createElement('a');
520
+ a.href = url;
521
+ a.download = fileName.endsWith('.md') ? fileName : fileName + '.md';
522
+ document.body.appendChild(a);
523
+ a.click();
524
+ document.body.removeChild(a);
525
+ URL.revokeObjectURL(url);
526
+ };
527
+
528
+ const handleDownloadPodcast = (audioUrl: string, fileName: string) => {
529
+ const a = document.createElement('a');
530
+ a.href = audioUrl;
531
+ a.download = fileName.endsWith('.mp3') ? fileName : fileName + '.mp3';
532
+ document.body.appendChild(a);
533
+ a.click();
534
+ document.body.removeChild(a);
535
+ };
536
+
537
+ const handleEditDocument = (content: string, title: string) => {
538
+ setEditingDocument({ content, title });
539
+ };
540
+
541
+ const handleNovaConversa = () => {
542
+ setMensagens([]);
543
+ setConversaAtual(null);
544
+ setAbaSelecionada('chat');
545
+ };
546
+
547
+ const handleSelecionarConversa = (conversaId: string) => {
548
+ const conversa = conversas.find(c => c.id === conversaId);
549
+ if (conversa) {
550
+ setMensagens(conversa.mensagens);
551
+ setConversaAtual(conversaId);
552
+ setAbaSelecionada('chat');
553
+ }
554
+ };
555
+
556
+ const handleToggleFavoritaConversa = (conversaId: string) => {
557
+ setConversas(prev =>
558
+ prev.map(conv =>
559
+ conv.id === conversaId ? { ...conv, favorita: !conv.favorita } : conv
560
+ )
561
+ );
562
+ };
563
+
564
+ const handleOpenSearchResult = (result: SearchResult) => {
565
+ console.log('Abrir resultado:', result);
566
+ // Implementar lógica de navegação ou abertura de resultado
567
+ };
568
+
569
+ const handleExecuteSearchCommand = async (
570
+ commandId: string,
571
+ searchTerm: string,
572
+ results: SearchResult[],
573
+ messageId: string
574
+ ) => {
575
+ setExecutingCommand(commandId);
576
+
577
+ // Simular execução de comando
578
+ setTimeout(() => {
579
+ if (commandId === '5') {
580
+ setSavedSearches(prev => [...prev, messageId]);
581
+ }
582
+ setExecutingCommand(null);
583
+ }, 1500);
584
+ };
585
+
586
+ // ============================================================================
587
+ // Computations
588
+ // ============================================================================
589
+
590
+ const conversasFiltradas = conversas.filter(conversa => {
591
+ if (abaSelecionada === 'favoritos') {
592
+ return conversa.favorita;
593
+ }
594
+ return true;
595
+ });
596
+
597
+ // ============================================================================
598
+ // Render
599
+ // ============================================================================
600
+
601
+ const containerWidth = width ?? (
602
+ isFullPage
603
+ ? '100%'
604
+ : isExpanded
605
+ ? (editingDocument ? '420px' : '100%')
606
+ : '64px' // Compacto quando fechado - apenas ícones
607
+ );
608
+ const containerHeight = height ?? 'h-full';
609
+
610
+ if (mobileFloating && !isExpanded) {
611
+ return (
612
+ <Button
613
+ onClick={handleToggle}
614
+ className={cn(
615
+ "h-12 rounded-full shadow-lg pl-2 pr-4 gap-2 bg-card text-foreground hover:bg-accent border border-border",
616
+ className
617
+ )}
618
+ >
619
+ <div className="bg-muted rounded-full p-0.5">
620
+ <XerticaOrbe size={32} />
621
+ </div>
622
+ <span className="font-medium">Assistente</span>
623
+ </Button>
624
+ );
625
+ }
626
+
627
+ return (
628
+ <>
629
+ {/* Hidden File Inputs */}
630
+ <input
631
+ ref={fileInputRef}
632
+ type="file"
633
+ accept=".pdf,.doc,.docx,.txt,.md"
634
+ className="hidden"
635
+ onChange={(e) => {
636
+ const file = e.target.files?.[0];
637
+ if (file && onFileAttach) {
638
+ onFileAttach(file);
639
+ }
640
+ }}
641
+ />
642
+ <input
643
+ ref={audioInputRef}
644
+ type="file"
645
+ accept="audio/*"
646
+ className="hidden"
647
+ onChange={(e) => {
648
+ const file = e.target.files?.[0];
649
+ if (file && onFileAttach) {
650
+ onFileAttach(file);
651
+ }
652
+ }}
653
+ />
654
+
655
+ {/* Document Editor */}
656
+ {editingDocument && !isFullPage && (
657
+ <div
658
+ className="flex-1 border-r border-border overflow-hidden"
659
+ >
660
+ <div className="h-full flex flex-col">
661
+ <div
662
+ className="px-4 py-3 border-b border-border bg-card flex items-center justify-between"
663
+ >
664
+ <h3 className="text-card-foreground">{editingDocument.title}</h3>
665
+ <Button
666
+ variant="ghost"
667
+ size="sm"
668
+ onClick={() => setEditingDocument(null)}
669
+ >
670
+ <X className="w-4 h-4" />
671
+ </Button>
672
+ </div>
673
+ <div className="flex-1 overflow-auto p-4">
674
+ <div
675
+ className="p-4 rounded-xl bg-muted"
676
+ >
677
+ <p className="text-muted-foreground">
678
+ Editor de documentos (simulado)
679
+ </p>
680
+ <pre className="mt-2 text-foreground whitespace-pre-wrap">
681
+ {editingDocument.content}
682
+ </pre>
683
+ </div>
684
+ </div>
685
+ </div>
686
+ </div>
687
+ )}
688
+
689
+ {/* Main Assistant Container */}
690
+ <div
691
+ className={`${containerHeight} flex flex-col ${className} bg-background ${!isFullPage ? 'border-l border-border shadow-sm' : ''}`}
692
+ style={{
693
+ width: isFullPage ? '100%' : containerWidth,
694
+ }}
695
+ >
696
+ {/* Header - Toggle Button - Apenas visível quando não está em fullPage */}
697
+ {!isFullPage && (
698
+ <div
699
+ className="border-b border-border flex items-center justify-between px-[14px] px-[18px] py-[16px]"
700
+ >
701
+ {isExpanded && (
702
+ <motion.div
703
+ initial={{ opacity: 0, x: 20 }}
704
+ animate={{ opacity: 1, x: 0 }}
705
+ exit={{ opacity: 0, x: 20 }}
706
+ className="flex items-center gap-2"
707
+ >
708
+ <XerticaOrbe size={32} />
709
+ <span className="text-foreground">Assistente Xertica</span>
710
+ </motion.div>
711
+ )}
712
+
713
+ <div className="flex items-center gap-1">
714
+ {isExpanded && onNavigateFullPage && (
715
+ <Button
716
+ variant="ghost"
717
+ size="sm"
718
+ onClick={onNavigateFullPage}
719
+ className="p-2 text-muted-foreground"
720
+ title="Expandir assistente"
721
+ >
722
+ <Maximize2 className="w-4 h-4" />
723
+ </Button>
724
+ )}
725
+
726
+ <Button
727
+ variant="ghost"
728
+ size="sm"
729
+ onClick={handleToggle}
730
+ className="p-2 text-muted-foreground"
731
+ >
732
+ {isExpanded ? (
733
+ <ChevronRight className="w-4 h-4" />
734
+ ) : (
735
+ <PanelRight className="w-4 h-4" />
736
+ )}
737
+ </Button>
738
+ </div>
739
+ </div>
740
+ )}
741
+
742
+ {/* Collapsed State - Icons Only */}
743
+ {!isExpanded && !isFullPage && (
744
+ <div className="flex flex-col items-center p-4 space-y-4">
745
+ {/* Ícone do Assistente - Xertica Orbe */}
746
+ <Tooltip>
747
+ <TooltipTrigger asChild>
748
+ <button
749
+ onClick={handleToggle}
750
+ className="flex items-center justify-center hover:opacity-90 transition-opacity duration-200 cursor-pointer"
751
+ >
752
+ <XerticaOrbe size={32} />
753
+ </button>
754
+ </TooltipTrigger>
755
+ <AssistantTooltipContent
756
+ side="left"
757
+ sideOffset={8}
758
+ >
759
+ <p>Assistente Xertica</p>
760
+ </AssistantTooltipContent>
761
+ </Tooltip>
762
+
763
+ <Tooltip>
764
+ <TooltipTrigger asChild>
765
+ <Button
766
+ variant="ghost"
767
+ size="sm"
768
+ onClick={() => handleExpandWithTab('chat')}
769
+ className="w-8 h-8 p-0 text-muted-foreground"
770
+ >
771
+ <MessageSquare className="w-4 h-4" />
772
+ </Button>
773
+ </TooltipTrigger>
774
+ <AssistantTooltipContent
775
+ side="left"
776
+ sideOffset={8}
777
+ >
778
+ <p>Chat</p>
779
+ </AssistantTooltipContent>
780
+ </Tooltip>
781
+
782
+ <Tooltip>
783
+ <TooltipTrigger asChild>
784
+ <Button
785
+ variant="ghost"
786
+ size="sm"
787
+ onClick={() => handleExpandWithTab('favoritos')}
788
+ className="w-8 h-8 p-0 text-muted-foreground"
789
+ >
790
+ <Heart className="w-4 h-4" />
791
+ </Button>
792
+ </TooltipTrigger>
793
+ <AssistantTooltipContent
794
+ side="left"
795
+ sideOffset={8}
796
+ >
797
+ <p>Favoritos</p>
798
+ </AssistantTooltipContent>
799
+ </Tooltip>
800
+
801
+ <Tooltip>
802
+ <TooltipTrigger asChild>
803
+ <Button
804
+ variant="ghost"
805
+ size="sm"
806
+ onClick={() => handleExpandWithTab('historico')}
807
+ className="w-8 h-8 p-0 text-muted-foreground"
808
+ >
809
+ <History className="w-4 h-4" />
810
+ </Button>
811
+ </TooltipTrigger>
812
+ <AssistantTooltipContent
813
+ side="left"
814
+ sideOffset={8}
815
+ >
816
+ <p>Histórico</p>
817
+ </AssistantTooltipContent>
818
+ </Tooltip>
819
+ </div>
820
+ )}
821
+
822
+ {/* Expanded State - Full Content */}
823
+ <AnimatePresence>
824
+ {(isExpanded || isFullPage) && (
825
+ <motion.div
826
+ initial={isFullPage ? false : { opacity: 0, x: 50 }}
827
+ animate={{ opacity: 1, x: 0 }}
828
+ exit={isFullPage ? undefined : { opacity: 0, x: 50 }}
829
+ transition={{ duration: 0.2 }}
830
+ className="flex-1 flex flex-col overflow-hidden"
831
+ >
832
+ {/* Navigation Tabs - Oculto em modo fullPage */}
833
+ {!isFullPage && (
834
+ <div
835
+ className="px-4 py-2 border-b border-border"
836
+ >
837
+ <div className="flex gap-1">
838
+ <Button
839
+ variant={abaSelecionada === 'chat' ? 'default' : 'ghost'}
840
+ size="sm"
841
+ onClick={() => setAbaSelecionada('chat')}
842
+ className="flex-1 h-8"
843
+ >
844
+ <MessageSquare className="w-3 h-3 mr-1" />
845
+ Chat
846
+ </Button>
847
+ <Button
848
+ variant={abaSelecionada === 'historico' ? 'default' : 'ghost'}
849
+ size="sm"
850
+ onClick={() => setAbaSelecionada('historico')}
851
+ className="flex-1 h-8"
852
+ >
853
+ <History className="w-3 h-3 mr-1" />
854
+ Histórico
855
+ </Button>
856
+ <Button
857
+ variant={abaSelecionada === 'favoritos' ? 'default' : 'ghost'}
858
+ size="sm"
859
+ onClick={() => setAbaSelecionada('favoritos')}
860
+ className="flex-1 h-8"
861
+ >
862
+ <Heart className="w-3 h-3 mr-1" />
863
+ Favoritos
864
+ </Button>
865
+ </div>
866
+ </div>
867
+ )}
868
+
869
+ {/* Content Area */}
870
+ <div className="flex-1 overflow-hidden flex flex-col">
871
+ {abaSelecionada === 'chat' && (
872
+ <div className={`flex-1 flex flex-col min-h-0 ${isFullPage ? 'mx-auto w-full max-w-6xl' : ''}`}>
873
+ {/* API Key Warning Banner */}
874
+ {!isApiKeyValid && showApiWarning && (
875
+ <div className="mx-4 mt-3 p-4 border-2 border-[var(--toast-warning-border)] rounded-xl bg-[var(--toast-warning-bg)]/20"
876
+ >
877
+ <div className="flex items-start gap-3">
878
+ <AlertCircle className="w-5 h-5 flex-shrink-0 mt-0.5 text-[var(--toast-warning-icon)]" />
879
+ <div className="flex-1 space-y-3">
880
+ <p className="text-foreground">
881
+ {!apiKey ? (
882
+ <strong>Nenhuma chave de API configurada</strong>
883
+ ) : (
884
+ <strong>🔐 Chave de API inválida ou vazada</strong>
885
+ )}
886
+ </p>
887
+ <p className="text-muted-foreground">
888
+ Configure uma nova chave do Google Gemini para ativar respostas inteligentes do assistente.
889
+ </p>
890
+ <div className="flex flex-wrap items-center gap-2">
891
+ {onNavigateSettings && (
892
+ <Button
893
+ onClick={onNavigateSettings}
894
+ size="sm"
895
+ className="bg-[var(--toast-warning-icon)] text-white hover:opacity-90 rounded-[var(--radius-button)]"
896
+ >
897
+ Ir para Configurações
898
+ </Button>
899
+ )}
900
+ <a
901
+ href="https://aistudio.google.com/apikey"
902
+ target="_blank"
903
+ rel="noopener noreferrer"
904
+ className="inline-flex items-center gap-1 underline text-muted-foreground hover:text-foreground"
905
+ >
906
+ Obter chave gratuitamente
907
+ <ExternalLink className="w-3 h-3" />
908
+ </a>
909
+ </div>
910
+ </div>
911
+ </div>
912
+ </div>
913
+ )}
914
+
915
+ {mensagens.length === 0 ? (
916
+ <div className="flex-1 overflow-y-auto min-h-0">
917
+ {/* Welcome Message */}
918
+ <div className="p-6 text-center">
919
+ <div className="mx-auto mb-4 flex items-center justify-center">
920
+ <XerticaOrbe size={64} />
921
+ </div>
922
+ <h3 className="mb-2 text-foreground">
923
+ Olá, {userName}
924
+ </h3>
925
+ <p className="text-muted-foreground">
926
+ Como posso ajudar?
927
+ </p>
928
+ </div>
929
+
930
+ {/* Suggestions */}
931
+ <div className="px-4 pb-4 space-y-2">
932
+ {sugestoes.map((sugestao) => (
933
+ <button
934
+ key={sugestao.id}
935
+ onClick={() => setMensagem(sugestao.texto)}
936
+ className="w-full p-3 text-left rounded-[var(--radius-card)] bg-muted text-foreground transition-colors duration-200 hover:bg-muted/80"
937
+ >
938
+ {sugestao.texto}
939
+ </button>
940
+ ))}
941
+
942
+ <Button
943
+ variant="ghost"
944
+ size="sm"
945
+ className="w-full justify-start text-muted-foreground"
946
+ >
947
+ <MoreHorizontal className="w-4 h-4 mr-2" />
948
+ Mais sugestões
949
+ </Button>
950
+ </div>
951
+ </div>
952
+ ) : (
953
+ <ScrollArea className="flex-1 min-h-0 overflow-x-hidden">
954
+ <div className="space-y-4 px-4 py-4 overflow-x-hidden max-w-6xl mx-auto w-full">
955
+ {mensagens.map((msg) => (
956
+ <motion.div
957
+ key={msg.id}
958
+ initial={{ opacity: 0, y: 10 }}
959
+ animate={{ opacity: 1, y: 0 }}
960
+ className={`flex gap-2 ${msg.type === 'user' ? 'justify-end' : 'justify-start'}`}
961
+ >
962
+ {/* Avatar do Assistente */}
963
+ {msg.type === 'assistant' && (
964
+ <div className="flex-shrink-0 pt-1">
965
+ <XerticaOrbe size={32} />
966
+ </div>
967
+ )}
968
+
969
+ <div className={cn(
970
+ "flex flex-col max-w-[85%] md:max-w-[70%] min-w-0 w-fit",
971
+ msg.type === 'user' ? "items-end" : "items-start"
972
+ )}>
973
+ <div
974
+ className={cn(
975
+ "px-4 py-2 break-words overflow-hidden overflow-x-hidden w-full rounded-2xl",
976
+ msg.type === 'user'
977
+ ? "bg-primary text-primary-foreground shadow-sm"
978
+ : "bg-muted text-foreground"
979
+ )}
980
+ >
981
+ {/* Document Header with Edit and Download Buttons */}
982
+ {msg.attachmentType === 'document' && (
983
+ <div
984
+ className="flex items-center justify-between mb-2 pb-2 border-b border-border min-w-0 overflow-hidden"
985
+ >
986
+ <div className="flex items-center gap-2 min-w-0 flex-1 overflow-hidden">
987
+ <FileText className="w-4 h-4 flex-shrink-0" />
988
+ <span className="text-sm font-medium break-words">{msg.attachmentName}</span>
989
+ </div>
990
+ <div className="flex items-center gap-1 flex-shrink-0">
991
+ <Button
992
+ variant="ghost"
993
+ size="sm"
994
+ onClick={() => msg.documentContent && msg.attachmentName && handleDownloadDocument(msg.documentContent, msg.attachmentName)}
995
+ className="h-6 px-2"
996
+ title="Download"
997
+ >
998
+ <Download className="w-3 h-3" />
999
+ </Button>
1000
+ <Button
1001
+ variant="ghost"
1002
+ size="sm"
1003
+ onClick={() => msg.documentContent && msg.documentTitle && handleEditDocument(msg.documentContent, msg.documentTitle)}
1004
+ className="h-6 px-2"
1005
+ >
1006
+ <Edit className="w-3 h-3 mr-1" />
1007
+ Editar
1008
+ </Button>
1009
+ </div>
1010
+ </div>
1011
+ )}
1012
+
1013
+ {/* Attachments */}
1014
+ {msg.attachmentType && msg.attachmentType !== 'podcast' && msg.attachmentType !== 'document' && (
1015
+ <div
1016
+ className="flex items-center gap-2 mb-2 pb-2 border-b border-border min-w-0 overflow-hidden"
1017
+ >
1018
+ {msg.attachmentType === 'file' && <FileText className="w-4 h-4 flex-shrink-0" />}
1019
+ {msg.attachmentType === 'audio' && <Music className="w-4 h-4 flex-shrink-0" />}
1020
+ {msg.attachmentType === 'image' && <ImageIcon className="w-4 h-4 flex-shrink-0" />}
1021
+ <span className="text-small break-words">{msg.attachmentName}</span>
1022
+ </div>
1023
+ )}
1024
+
1025
+ {/* Message Content */}
1026
+ {msg.type === 'user' ? (
1027
+ <p className="whitespace-pre-wrap break-words">{msg.content}</p>
1028
+ ) : (
1029
+ <>
1030
+ {(msg.content.includes('🔐') || msg.content.includes('❌')) && (
1031
+ <div
1032
+ className="mb-3 p-3 border rounded-[var(--radius)] overflow-hidden bg-[var(--toast-error-bg)]/20 border-[var(--toast-error-border)]/30"
1033
+ >
1034
+ <div className="flex items-start gap-2 min-w-0">
1035
+ <AlertCircle className="w-5 h-5 flex-shrink-0 mt-0.5 text-[var(--toast-error-icon)]" />
1036
+ <div className="flex-1 min-w-0 overflow-hidden">
1037
+ <MarkdownMessage
1038
+ content={msg.content}
1039
+ className="text-foreground"
1040
+ />
1041
+ </div>
1042
+ </div>
1043
+ </div>
1044
+ )}
1045
+ {!(msg.content.includes('🔐') || msg.content.includes('❌')) && (
1046
+ <MarkdownMessage
1047
+ content={msg.content}
1048
+ className="text-foreground"
1049
+ />
1050
+ )}
1051
+ </>
1052
+ )}
1053
+
1054
+ {/* Document Preview */}
1055
+ {msg.attachmentType === 'document' && msg.documentContent && (
1056
+ <div
1057
+ className="mt-3 pt-3 border-t border-border overflow-hidden"
1058
+ >
1059
+ <FormattedDocument
1060
+ content={msg.documentContent}
1061
+ maxPreviewLength={250}
1062
+ />
1063
+ </div>
1064
+ )}
1065
+
1066
+ {/* Podcast Player */}
1067
+ {msg.attachmentType === 'podcast' && msg.audioUrl && (
1068
+ <div
1069
+ className="mt-3 pt-3 border-t border-border overflow-hidden"
1070
+ >
1071
+ <div className="flex items-center justify-between mb-3 min-w-0 overflow-hidden">
1072
+ <div className="flex items-center gap-2 min-w-0 flex-1 overflow-hidden">
1073
+ <div
1074
+ className="w-6 h-6 flex items-center justify-center flex-shrink-0 rounded-[var(--radius-button)] bg-gradient-to-br from-[var(--chart-1)] via-[var(--chart-4)] to-[var(--chart-1)]"
1075
+ >
1076
+ <Radio className="w-3 h-3 text-white" />
1077
+ </div>
1078
+ <span className="text-sm font-medium break-words">
1079
+ {msg.attachmentName}
1080
+ </span>
1081
+ </div>
1082
+ <Button
1083
+ variant="ghost"
1084
+ size="sm"
1085
+ onClick={() => msg.audioUrl && msg.attachmentName && handleDownloadPodcast(msg.audioUrl, msg.attachmentName)}
1086
+ className="h-6 px-2 flex-shrink-0"
1087
+ title="Download"
1088
+ >
1089
+ <Download className="w-3 h-3" />
1090
+ </Button>
1091
+ </div>
1092
+ <div
1093
+ className="rounded-[var(--radius)] p-2 overflow-hidden bg-gradient-to-r from-[var(--chart-1)]/10 to-[var(--chart-4)]/10"
1094
+ >
1095
+ <audio
1096
+ controls
1097
+ className="w-full max-w-full h-10 outline-none"
1098
+ >
1099
+ <source src={msg.audioUrl} type="audio/mpeg" />
1100
+ Seu navegador não suporta o elemento de áudio.
1101
+ </audio>
1102
+ </div>
1103
+ </div>
1104
+ )}
1105
+
1106
+ {/* Search Results */}
1107
+ {msg.attachmentType === 'search' && msg.searchResults && (
1108
+ <div
1109
+ className="mt-3 pt-3 border-t border-border space-y-4 overflow-hidden"
1110
+ >
1111
+ <div className="overflow-hidden">
1112
+ <div className="flex items-center gap-2 mb-3">
1113
+ <Search className="w-4 h-4 text-[var(--chart-4)]" />
1114
+ <h4 className="text-foreground">
1115
+ Resultados Encontrados ({msg.searchResults.length})
1116
+ </h4>
1117
+ </div>
1118
+ <div className="space-y-2 overflow-hidden">
1119
+ {msg.searchResults.map((result) => (
1120
+ <div
1121
+ key={result.id}
1122
+ onClick={() => handleOpenSearchResult(result)}
1123
+ className="p-3 rounded-[var(--radius)] border border-border transition-all cursor-pointer group overflow-hidden hover:bg-muted/50"
1124
+ >
1125
+ <div className="flex items-start justify-between gap-2 min-w-0">
1126
+ <div className="flex items-start gap-2 flex-1 min-w-0 overflow-hidden">
1127
+ <div className="mt-0.5 flex-shrink-0">
1128
+ {result.type === 'document' && <FileText className="w-4 h-4 text-[var(--chart-4)]" />}
1129
+ {result.type === 'project' && <FolderOpen className="w-4 h-4 text-[var(--chart-1)]" />}
1130
+ {result.type === 'conversation' && <MessageSquare className="w-4 h-4 text-[var(--chart-2)]" />}
1131
+ {result.type === 'file' && <Folder className="w-4 h-4 text-[var(--chart-3)]" />}
1132
+ {result.type === 'contact' && <Users className="w-4 h-4 text-[var(--chart-5)]" />}
1133
+ </div>
1134
+ <div className="flex-1 min-w-0 overflow-hidden">
1135
+ <div className="flex items-start gap-2 min-w-0">
1136
+ <h5 className="text-sm font-medium break-words flex-1 min-w-0 text-foreground">
1137
+ {result.title}
1138
+ </h5>
1139
+ <span
1140
+ className="px-1.5 py-0.5 rounded-sm flex-shrink-0 self-start bg-[var(--chart-4)]/10 text-[var(--chart-4)]"
1141
+ >
1142
+ {result.relevance}%
1143
+ </span>
1144
+ </div>
1145
+ <p className="text-sm text-muted-foreground mt-1 line-clamp-2 break-words">
1146
+ {result.description}
1147
+ </p>
1148
+ <div className="flex items-center gap-3 mt-2 flex-wrap min-w-0">
1149
+ <span className="text-sm text-muted-foreground flex items-center gap-1 min-w-0 max-w-full">
1150
+ <ExternalLink className="w-3 h-3 flex-shrink-0" />
1151
+ <span className="truncate">{result.path}</span>
1152
+ </span>
1153
+ {result.lastModified && (
1154
+ <span className="text-sm text-muted-foreground flex items-center gap-1 flex-shrink-0">
1155
+ <Clock className="w-3 h-3" />
1156
+ {result.lastModified}
1157
+ </span>
1158
+ )}
1159
+ </div>
1160
+ </div>
1161
+ </div>
1162
+ </div>
1163
+ </div>
1164
+ ))}
1165
+ </div>
1166
+ </div>
1167
+
1168
+ {/* Search Sources */}
1169
+ {msg.searchSources && (
1170
+ <div>
1171
+ <h4 className="mb-2 text-foreground">
1172
+ Fontes Consultadas
1173
+ </h4>
1174
+ <div className="flex flex-wrap gap-2 overflow-hidden">
1175
+ {msg.searchSources.map((source, index) => (
1176
+ <div
1177
+ key={index}
1178
+ className="px-3 py-1.5 border max-w-full rounded-[var(--radius-button)] bg-muted border-border"
1179
+ >
1180
+ <span className="text-sm font-medium text-foreground break-words">{source.name}</span>
1181
+ <span className="text-sm text-muted-foreground ml-1 whitespace-nowrap">({source.count})</span>
1182
+ </div>
1183
+ ))}
1184
+ </div>
1185
+ </div>
1186
+ )}
1187
+
1188
+ {/* Search Commands */}
1189
+ {msg.searchCommands && msg.searchResults && (
1190
+ <div>
1191
+ <h4 className="mb-2 text-foreground">
1192
+ O que você pode fazer com estes resultados
1193
+ </h4>
1194
+ <div className="grid grid-cols-1 gap-2">
1195
+ {msg.searchCommands.map((command) => (
1196
+ <button
1197
+ key={command.id}
1198
+ onClick={() => handleExecuteSearchCommand(
1199
+ command.id,
1200
+ msg.content.replace('🔍 Pesquisa realizada com sucesso!', '').split('"')[1] || 'pesquisa',
1201
+ msg.searchResults!,
1202
+ msg.id
1203
+ )}
1204
+ disabled={executingCommand === command.id}
1205
+ className={cn(
1206
+ "flex items-start gap-2 p-2 rounded-[var(--radius)] border transition-all text-left disabled:opacity-50 disabled:cursor-not-allowed",
1207
+ savedSearches.includes(msg.id) && command.id === '5'
1208
+ ? "border-[var(--toast-warning-border)] bg-[var(--toast-warning-bg)]/20"
1209
+ : "border-border bg-transparent hover:bg-muted/50"
1210
+ )}
1211
+ >
1212
+ {executingCommand === command.id ? (
1213
+ <Loader2 className="w-5 h-5 animate-spin flex-shrink-0 text-primary" />
1214
+ ) : (
1215
+ <span className="flex-shrink-0">{command.icon}</span>
1216
+ )}
1217
+ <div className="flex-1 min-w-0 overflow-hidden">
1218
+ <div className="text-sm font-medium text-foreground break-words">
1219
+ {command.id === '5' && savedSearches.includes(msg.id) ? '⭐ Pesquisa salva' : command.label}
1220
+ </div>
1221
+ <div className="text-sm text-muted-foreground break-words">
1222
+ {executingCommand === command.id ? 'Processando...' : command.description}
1223
+ </div>
1224
+ </div>
1225
+ </button>
1226
+ ))}
1227
+ </div>
1228
+ </div>
1229
+ )}
1230
+ </div>
1231
+ )}
1232
+ </div>
1233
+
1234
+ {/* Message Actions */}
1235
+ <div className="flex items-center gap-2 mt-1 px-2">
1236
+ <span className="text-sm text-muted-foreground">
1237
+ {msg.timestamp.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}
1238
+ </span>
1239
+
1240
+ <Button
1241
+ variant="ghost"
1242
+ size="sm"
1243
+ onClick={() => handleToggleFavorite(msg.id)}
1244
+ className={`h-6 w-6 p-0 ${msg.isFavorite ? 'text-destructive' : 'text-muted-foreground'}`}
1245
+ >
1246
+ <Heart className={`w-3 h-3 ${msg.isFavorite ? 'fill-current' : ''}`} />
1247
+ </Button>
1248
+
1249
+ {msg.type === 'assistant' && msg.attachmentType !== 'podcast' && (
1250
+ <Button
1251
+ variant="ghost"
1252
+ size="sm"
1253
+ onClick={() => handleGeneratePodcast(msg.id, msg.content)}
1254
+ disabled={generatingPodcastId === msg.id}
1255
+ className="h-6 w-6 p-0 disabled:opacity-50 text-muted-foreground"
1256
+ title="Gerar Podcast"
1257
+ >
1258
+ {generatingPodcastId === msg.id ? (
1259
+ <Loader2 className="w-3 h-3 animate-spin" />
1260
+ ) : (
1261
+ <Radio className="w-3 h-3" />
1262
+ )}
1263
+ </Button>
1264
+ )}
1265
+
1266
+ <Button
1267
+ variant="ghost"
1268
+ size="sm"
1269
+ onClick={() => handleCopyMessage(msg.content, msg.id)}
1270
+ className={`h-6 w-6 p-0 ${copiedId === msg.id ? 'text-[var(--toast-success-icon)]' : 'text-muted-foreground'}`}
1271
+ >
1272
+ {copiedId === msg.id ? (
1273
+ <Check className="w-3 h-3" />
1274
+ ) : (
1275
+ <Copy className="w-3 h-3" />
1276
+ )}
1277
+ </Button>
1278
+ </div>
1279
+ </div>
1280
+ </motion.div>
1281
+ ))}
1282
+
1283
+ {/* Typing Indicator */}
1284
+ {isProcessing && (
1285
+ <motion.div
1286
+ initial={{ opacity: 0, y: 10 }}
1287
+ animate={{ opacity: 1, y: 0 }}
1288
+ className="flex gap-2 justify-start"
1289
+ >
1290
+ {/* Avatar do Assistente */}
1291
+ <div className="flex-shrink-0 pt-1">
1292
+ <XerticaOrbe size={32} />
1293
+ </div>
1294
+
1295
+ <div
1296
+ className="px-4 py-3 bg-muted rounded-2xl"
1297
+ >
1298
+ <div className="flex gap-1">
1299
+ <motion.div
1300
+ className="w-2 h-2 rounded-full bg-muted-foreground"
1301
+ animate={{ y: [0, -8, 0] }}
1302
+ transition={{ repeat: Infinity, duration: 0.6, delay: 0 }}
1303
+ />
1304
+ <motion.div
1305
+ className="w-2 h-2 rounded-full bg-muted-foreground"
1306
+ animate={{ y: [0, -8, 0] }}
1307
+ transition={{ repeat: Infinity, duration: 0.6, delay: 0.2 }}
1308
+ />
1309
+ <motion.div
1310
+ className="w-2 h-2 rounded-full bg-muted-foreground"
1311
+ animate={{ y: [0, -8, 0] }}
1312
+ transition={{ repeat: Infinity, duration: 0.6, delay: 0.4 }}
1313
+ />
1314
+ </div>
1315
+ </div>
1316
+ </motion.div>
1317
+ )}
1318
+
1319
+ <div ref={messagesEndRef} />
1320
+ </div>
1321
+ </ScrollArea>
1322
+ )}
1323
+ </div>
1324
+ )}
1325
+
1326
+ {(abaSelecionada === 'historico' || abaSelecionada === 'favoritos') && (
1327
+ <ScrollArea className="flex-1 min-h-0">
1328
+ <div className={`p-4 ${isFullPage ? 'mx-auto w-full max-w-6xl' : ''}`}>
1329
+ {/* New Conversation Button */}
1330
+ <Button
1331
+ variant="outline"
1332
+ size="sm"
1333
+ onClick={handleNovaConversa}
1334
+ className="w-full mb-4 justify-start"
1335
+ >
1336
+ <Plus className="w-4 h-4 mr-2" />
1337
+ Nova Conversa
1338
+ </Button>
1339
+
1340
+ {/* Conversations List */}
1341
+ <div className="space-y-2">
1342
+ {conversasFiltradas.length === 0 ? (
1343
+ <div className="text-center py-8">
1344
+ <Heart className="w-12 h-12 mx-auto text-muted-foreground/50 mb-2" />
1345
+ <p className="text-muted-foreground">
1346
+ {abaSelecionada === 'favoritos'
1347
+ ? 'Nenhuma conversa favorita ainda'
1348
+ : 'Nenhuma conversa no histórico'}
1349
+ </p>
1350
+ </div>
1351
+ ) : (
1352
+ conversasFiltradas.map((conversa) => (
1353
+ <div
1354
+ key={conversa.id}
1355
+ onClick={() => handleSelecionarConversa(conversa.id)}
1356
+ className={cn(
1357
+ "p-3 rounded-[var(--radius)] cursor-pointer transition-colors duration-200 border",
1358
+ conversa.id === conversaAtual
1359
+ ? "border-primary bg-primary/10"
1360
+ : "border-border hover:bg-muted"
1361
+ )}
1362
+ >
1363
+ <div className="flex items-start justify-between mb-1">
1364
+ <h4 className="text-sm font-medium text-foreground truncate flex-1">
1365
+ {conversa.titulo}
1366
+ </h4>
1367
+ <Button
1368
+ variant="ghost"
1369
+ size="sm"
1370
+ onClick={(e) => {
1371
+ e.stopPropagation();
1372
+ handleToggleFavoritaConversa(conversa.id);
1373
+ }}
1374
+ className="h-6 w-6 p-0 flex-shrink-0 ml-1"
1375
+ >
1376
+ <Heart
1377
+ className={cn(
1378
+ "w-3 h-3",
1379
+ conversa.favorita ? "text-destructive fill-current" : "text-muted-foreground"
1380
+ )}
1381
+ />
1382
+ </Button>
1383
+ </div>
1384
+ {conversa.ultimaMensagem && (
1385
+ <p className="text-sm text-muted-foreground truncate mb-1">
1386
+ {conversa.ultimaMensagem}
1387
+ </p>
1388
+ )}
1389
+ <p className="text-sm text-muted-foreground">
1390
+ {conversa.timestamp}
1391
+ </p>
1392
+ </div>
1393
+ ))
1394
+ )}
1395
+ </div>
1396
+ </div>
1397
+ </ScrollArea>
1398
+ )}
1399
+ </div>
1400
+
1401
+ {/* Modern Input Area - Only visible in chat mode */}
1402
+ {abaSelecionada === 'chat' && (
1403
+ <ModernChatInput
1404
+ value={mensagem}
1405
+ onChange={setMensagem}
1406
+ onSubmit={handleEnviarMensagem}
1407
+ onFileUpload={() => fileInputRef.current?.click()}
1408
+ onAudioUpload={() => audioInputRef.current?.click()}
1409
+ placeholder="Envie uma mensagem para Xertica"
1410
+ disabled={isProcessing}
1411
+ isFullPage={isFullPage}
1412
+ />
1413
+ )}
1414
+ </motion.div>
1415
+ )}
1416
+ </AnimatePresence>
1417
+ </div>
1418
+ </>
1419
+ );
1420
+ }