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,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
|
+
}
|