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,2003 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router';
|
|
3
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
4
|
+
import {
|
|
5
|
+
MessageSquare,
|
|
6
|
+
Heart,
|
|
7
|
+
History,
|
|
8
|
+
MoreHorizontal,
|
|
9
|
+
X,
|
|
10
|
+
ChevronLeft,
|
|
11
|
+
ChevronRight,
|
|
12
|
+
PanelRight,
|
|
13
|
+
Plus,
|
|
14
|
+
Copy,
|
|
15
|
+
Check,
|
|
16
|
+
FileText,
|
|
17
|
+
Music,
|
|
18
|
+
Image as ImageIcon,
|
|
19
|
+
Radio,
|
|
20
|
+
Loader2,
|
|
21
|
+
Edit,
|
|
22
|
+
Download,
|
|
23
|
+
Search,
|
|
24
|
+
Folder,
|
|
25
|
+
Users,
|
|
26
|
+
ExternalLink,
|
|
27
|
+
Star,
|
|
28
|
+
Clock,
|
|
29
|
+
FolderOpen,
|
|
30
|
+
Filter,
|
|
31
|
+
Mail,
|
|
32
|
+
BarChart3,
|
|
33
|
+
Bookmark,
|
|
34
|
+
Maximize2,
|
|
35
|
+
AlertCircle
|
|
36
|
+
} from 'lucide-react';
|
|
37
|
+
import { Button } from './ui/button';
|
|
38
|
+
import { ScrollArea } from './ui/scroll-area';
|
|
39
|
+
import { ModernChatInput, ActionType } from './ModernChatInput';
|
|
40
|
+
import { Separator } from './ui/separator';
|
|
41
|
+
import { DocumentEditor } from './DocumentEditor';
|
|
42
|
+
import { FormattedDocument } from './FormattedDocument';
|
|
43
|
+
import { MarkdownMessage } from './MarkdownMessage';
|
|
44
|
+
import { XerticaOrbe } from './XerticaOrbe';
|
|
45
|
+
import { useApiKey } from '../contexts/ApiKeyContext';
|
|
46
|
+
import { useAssistente } from '../contexts/AssistenteContext';
|
|
47
|
+
import { callGeminiAPI, buildSystemPrompt, GeminiMessage } from '../utils/gemini';
|
|
48
|
+
import { toast } from 'sonner';
|
|
49
|
+
import { Tooltip, TooltipTrigger } from './ui/tooltip';
|
|
50
|
+
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
|
51
|
+
import { cn } from './ui/utils';
|
|
52
|
+
|
|
53
|
+
// Tooltip customizado estilo sidebar (branco)
|
|
54
|
+
function AssistantTooltipContent({
|
|
55
|
+
className,
|
|
56
|
+
sideOffset = 0,
|
|
57
|
+
children,
|
|
58
|
+
...props
|
|
59
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
60
|
+
return (
|
|
61
|
+
<TooltipPrimitive.Portal>
|
|
62
|
+
<TooltipPrimitive.Content
|
|
63
|
+
data-slot="tooltip-content"
|
|
64
|
+
sideOffset={sideOffset}
|
|
65
|
+
className={cn(
|
|
66
|
+
"bg-card text-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",
|
|
67
|
+
className,
|
|
68
|
+
)}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
<TooltipPrimitive.Arrow
|
|
73
|
+
className="fill-card z-50 drop-shadow-sm"
|
|
74
|
+
width={8}
|
|
75
|
+
height={4}
|
|
76
|
+
/>
|
|
77
|
+
</TooltipPrimitive.Content>
|
|
78
|
+
</TooltipPrimitive.Portal>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface AssistenteXerticaProps {
|
|
83
|
+
isExpanded: boolean;
|
|
84
|
+
onToggle: () => void;
|
|
85
|
+
onToggleWithTab?: (tab: 'chat' | 'historico' | 'favoritos') => void;
|
|
86
|
+
isFullPage?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
import type { Message, Conversa, SearchResult, SearchSource, SearchCommand } from '../contexts/AssistenteContext';
|
|
90
|
+
|
|
91
|
+
interface Sugestao {
|
|
92
|
+
id: string;
|
|
93
|
+
texto: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const sugestoesPadrao: Sugestao[] = [
|
|
97
|
+
{ id: '1', texto: 'O que posso pedir para você fazer?' },
|
|
98
|
+
{ id: '2', texto: 'O que você faz?' },
|
|
99
|
+
{ id: '3', texto: 'Com quais projetos devo me preocupar agora?' },
|
|
100
|
+
{ id: '4', texto: 'Qual meu próximo projeto?' },
|
|
101
|
+
{ id: '5', texto: 'Qual projetos está tendo o melhor desempenho?' }
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// Respostas simuladas da IA
|
|
105
|
+
const gerarResposta = (mensagemUsuario: string): string => {
|
|
106
|
+
const mensagemLower = mensagemUsuario.toLowerCase();
|
|
107
|
+
const mensagemOriginal = mensagemUsuario;
|
|
108
|
+
|
|
109
|
+
if (mensagemLower.includes('o que') && (mensagemLower.includes('fazer') || mensagemLower.includes('pedir'))) {
|
|
110
|
+
return 'Posso ajudar você com diversas tarefas! Posso:\n\n• Analisar dados e métricas dos seus projetos\n• Responder perguntas sobre performance e resultados\n• Sugerir otimizações e melhorias\n• Gerar relatórios e documentação\n• Ajudar no planejamento de sprints\n• E muito mais!\n\nQual tarefa você gostaria de realizar primeiro?';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (mensagemLower.includes('o que você faz') || mensagemLower.includes('quem é você')) {
|
|
114
|
+
return 'Olá! Sou o Assistente Xertica, uma IA desenvolvida para ajudar você a gerenciar projetos, analisar dados e otimizar processos. Estou aqui 24/7 para responder suas perguntas e auxiliar nas suas tarefas diárias.\n\nPosso processar documentos, analisar áudios e imagens, além de fornecer insights baseados nos dados da plataforma.';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (mensagemLower.includes('projeto') && (mensagemLower.includes('preocupar') || mensagemLower.includes('atenção'))) {
|
|
118
|
+
return 'Com base na análise dos seus projetos ativos, recomendo focar nos seguintes:\n\n1. **Projeto Alpha** - 15% acima do prazo, requer atenção imediata\n2. **Sistema Beta** - Performance crítica, necessita otimização\n3. **Mobile Gamma** - Aguardando aprovações há 5 dias\n\nGostaria de mais detalhes sobre algum deles?';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (mensagemLower.includes('próximo projeto') || mensagemLower.includes('próxima tarefa')) {
|
|
122
|
+
return 'Seu próximo projeto prioritário é o **Sistema de Analytics V2**.\n\n📅 Início previsto: Próxima segunda-feira\n👥 Time: 5 desenvolvedores\n⏱️ Duração estimada: 3 sprints\n\nJá preparei um roadmap inicial. Gostaria de revisar?';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (mensagemLower.includes('desempenho') || mensagemLower.includes('performance') || mensagemLower.includes('melhor')) {
|
|
126
|
+
return 'Analisando os dados de performance dos últimos 30 dias:\n\n🏆 **Melhor Performance:**\n• Projeto Dashboard 2.0: +35% eficiência\n• Sistema CRM: -40% tempo de resposta\n• App Mobile: 4.8★ rating (+0.5)\n\n📊 Todos estão acima das metas estabelecidas. Parab��ns!\n\nQuer ver métricas detalhadas?';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (mensagemLower.includes('olá') || mensagemLower.includes('oi') || mensagemLower.includes('bom dia') || mensagemLower.includes('boa tarde') || mensagemLower.includes('boa noite')) {
|
|
130
|
+
return 'Olá! 👋 Como posso ajudar você hoje? Estou pronto para auxiliar com análises, relatórios ou responder suas dúvidas sobre os projetos.';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (mensagemLower.includes('obrigado') || mensagemLower.includes('obrigada')) {
|
|
134
|
+
return 'Por nada! Estou aqui sempre que precisar. 😊 Se tiver mais alguma dúvida ou precisar de ajuda, é só chamar!';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Respostas para ações especiais
|
|
138
|
+
if (mensagemLower.includes('criar documento')) {
|
|
139
|
+
const tema = mensagemOriginal.replace(/📄 \[Criar documento\]/gi, '').trim();
|
|
140
|
+
return `📝 Documento criado com sucesso!\\n\\nGerei um documento completo sobre "${tema}" com as seguintes seções:\\n\\n• Introdução e contexto\\n• Análise detalhada\\n• Dados e métricas relevantes\\n• Conclusões e recomendações\\n• Próximos passos\\n\\nO documento está pronto para revisão e pode ser editado conforme necessário. Gostaria de adicionar ou modificar alguma seção?`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (mensagemLower.includes('gerar podcast')) {
|
|
144
|
+
const tema = mensagemOriginal.replace(/🎙️ \[Gerar podcast\]/gi, '').trim();
|
|
145
|
+
return `🎙️ Preparando podcast sobre "${tema}"...\\n\\nEstou processando o conteúdo e gerando um roteiro de podcast profissional com:\\n\\n• Introdução envolvente\\n• Desenvolvimento do tema\\n• Exemplos práticos\\n• Conclusão e insights\\n\\nO áudio será gerado em instantes. Aguarde...`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (mensagemLower.includes('pesquisar')) {
|
|
149
|
+
const termo = mensagemOriginal.replace(/🔍 \[Pesquisar\]/gi, '').trim();
|
|
150
|
+
return `🔍 Resultados da pesquisa sobre "${termo}"\\n\\n**Encontrei as seguintes informações relevantes:**\\n\\n1. **Documentação interna** - 12 resultados\\n Guias e manuais relacionados ao tema\\n\\n2. **Projetos relacionados** - 8 projetos\\n Incluindo Analytics v2 e Dashboard Pro\\n\\n3. **Discussões em equipe** - 15 menções\\n Últimas conversas sobre o assunto\\n\\nGostaria de ver mais detalhes sobre algum desses resultados?`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (mensagemLower.includes('arquivo') || mensagemLower.includes('documento')) {
|
|
154
|
+
return 'Entendi que você deseja trabalhar com arquivos. Posso analisar diversos tipos de documentos:\n\n📄 Documentos de texto (PDF, DOCX)\n📊 Planilhas (XLSX, CSV)\n📈 Relatórios e apresentações\n\nBasta enviá-los usando o botão de anexo (📎) e terei prazer em analisá-los para você!';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Resposta genérica inteligente
|
|
158
|
+
const respostasGenericas = [
|
|
159
|
+
'Entendo sua questão. Com base nos dados disponíveis na plataforma Xertica, posso fornecer análises detalhadas sobre esse tema. Poderia me dar mais contexto para que eu possa ajudá-lo melhor?',
|
|
160
|
+
'Interessante! Deixe-me processar isso... Com base no seu histórico e nos dados do sistema, recomendo que possamos explorar essa questão em mais detalhes. O que especificamente você gostaria de saber?',
|
|
161
|
+
'Ótima pergunta! Para te dar a melhor resposta possível, preciso entender melhor o contexto. Você pode me fornecer mais informações sobre o que está buscando?',
|
|
162
|
+
'Estou analisando sua solicitação. Baseado nos dados do sistema Xertica, posso te ajudar com isso. Você gostaria de uma análise rápida ou um relatório completo?'
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
return respostasGenericas[Math.floor(Math.random() * respostasGenericas.length)];
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export function AssistenteXertica({ isExpanded, onToggle, onToggleWithTab, isFullPage = false }: AssistenteXerticaProps) {
|
|
169
|
+
const navigate = useNavigate();
|
|
170
|
+
const { geminiApiKey, isApiKeyValid } = useApiKey();
|
|
171
|
+
const {
|
|
172
|
+
conversaAtual,
|
|
173
|
+
setConversaAtual,
|
|
174
|
+
conversas,
|
|
175
|
+
setConversas,
|
|
176
|
+
isTyping,
|
|
177
|
+
setIsTyping,
|
|
178
|
+
abaSelecionada,
|
|
179
|
+
setAbaSelecionada,
|
|
180
|
+
editingDocument,
|
|
181
|
+
setEditingDocument,
|
|
182
|
+
searchFilter,
|
|
183
|
+
setSearchFilter,
|
|
184
|
+
savedSearches,
|
|
185
|
+
setSavedSearches,
|
|
186
|
+
} = useAssistente();
|
|
187
|
+
|
|
188
|
+
const [mensagem, setMensagem] = useState('');
|
|
189
|
+
const [sugestoes] = useState<Sugestao[]>(sugestoesPadrao);
|
|
190
|
+
const [copiedId, setCopiedId] = useState<string | null>(null);
|
|
191
|
+
const [generatingPodcastId, setGeneratingPodcastId] = useState<string | null>(null);
|
|
192
|
+
const [executingCommand, setExecutingCommand] = useState<string | null>(null);
|
|
193
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
194
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
195
|
+
const audioInputRef = useRef<HTMLInputElement>(null);
|
|
196
|
+
|
|
197
|
+
const conversaAtiva = conversas.find(c => c.id === conversaAtual);
|
|
198
|
+
const mensagens = conversaAtiva?.mensagens || [];
|
|
199
|
+
|
|
200
|
+
// Auto-scroll para última mensagem
|
|
201
|
+
const scrollToBottom = () => {
|
|
202
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
scrollToBottom();
|
|
207
|
+
}, [mensagens, isTyping]);
|
|
208
|
+
|
|
209
|
+
// Resposta da IA usando Gemini API
|
|
210
|
+
const simularRespostaIA = async (mensagemUsuario: string) => {
|
|
211
|
+
setIsTyping(true);
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
let resposta: string;
|
|
215
|
+
|
|
216
|
+
// Se a chave da API for válida, usar Gemini
|
|
217
|
+
if (isApiKeyValid && geminiApiKey) {
|
|
218
|
+
// Construir histórico da conversa para o Gemini
|
|
219
|
+
const conversaAtiva = conversas.find(c => c.id === conversaAtual);
|
|
220
|
+
const historico: GeminiMessage[] = [];
|
|
221
|
+
|
|
222
|
+
// Adicionar prompt do sistema
|
|
223
|
+
const systemPrompt = buildSystemPrompt();
|
|
224
|
+
|
|
225
|
+
// Converter mensagens anteriores para formato Gemini (últimas 10 mensagens)
|
|
226
|
+
const mensagensRecentes = (conversaAtiva?.mensagens || []).slice(-10);
|
|
227
|
+
for (const msg of mensagensRecentes) {
|
|
228
|
+
if (msg.type === 'user') {
|
|
229
|
+
// Remover prefixos de ação antes de enviar ao Gemini
|
|
230
|
+
let conteudoLimpo = msg.content
|
|
231
|
+
.replace(/^📄 \[Criar documento\] /, '')
|
|
232
|
+
.replace(/^🎙️ \[Gerar podcast\] /, '')
|
|
233
|
+
.replace(/^🔍 \[Pesquisar\] /, '');
|
|
234
|
+
|
|
235
|
+
historico.push({
|
|
236
|
+
role: 'user',
|
|
237
|
+
parts: [{ text: conteudoLimpo }]
|
|
238
|
+
});
|
|
239
|
+
} else if (msg.type === 'assistant') {
|
|
240
|
+
historico.push({
|
|
241
|
+
role: 'model',
|
|
242
|
+
parts: [{ text: msg.content }]
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Se não houver histórico, adicionar o prompt do sistema como primeira mensagem
|
|
248
|
+
if (historico.length === 0) {
|
|
249
|
+
historico.push({
|
|
250
|
+
role: 'user',
|
|
251
|
+
parts: [{ text: systemPrompt }]
|
|
252
|
+
});
|
|
253
|
+
historico.push({
|
|
254
|
+
role: 'model',
|
|
255
|
+
parts: [{ text: 'Entendido! Estou pronto para ajudar você com a plataforma Xertica.ai. Como posso ajudá-lo hoje?' }]
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
resposta = await callGeminiAPI(geminiApiKey, mensagemUsuario, historico);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error('Erro ao chamar API Gemini:', error);
|
|
263
|
+
// Usar a mensagem de erro do utilitário se disponível
|
|
264
|
+
const errorMessage = error instanceof Error ? error.message : '❌ Erro desconhecido';
|
|
265
|
+
|
|
266
|
+
// Verificar se é erro de chave vazada ou 403
|
|
267
|
+
const isLeakedKeyError = errorMessage.toLowerCase().includes('leaked') ||
|
|
268
|
+
errorMessage.toLowerCase().includes('reported') ||
|
|
269
|
+
errorMessage.toLowerCase().includes('403');
|
|
270
|
+
|
|
271
|
+
if (isLeakedKeyError) {
|
|
272
|
+
toast.error('Chave de API desativada por segurança', {
|
|
273
|
+
description: 'Sua chave foi reportada como vazada. Gere uma nova em Google AI Studio.',
|
|
274
|
+
duration: 6000,
|
|
275
|
+
});
|
|
276
|
+
resposta = `🔐 **Chave de API Desativada por Segurança**\n\n${errorMessage}\n\n**Como resolver:**\n1. Acesse [Google AI Studio](https://aistudio.google.com/apikey)\n2. Gere uma nova chave de API\n3. Configure a nova chave em **Configurações > API**\n\n⚠️ **Importante:** Não compartilhe sua chave de API publicamente.`;
|
|
277
|
+
} else {
|
|
278
|
+
toast.error('Erro ao processar mensagem', {
|
|
279
|
+
description: 'Verifique sua chave de API nas Configurações.',
|
|
280
|
+
duration: 4000,
|
|
281
|
+
});
|
|
282
|
+
resposta = `**Erro ao processar sua mensagem**\n\n${errorMessage}\n\n💡 **Dica:** Verifique se sua chave de API está configurada corretamente em **Configurações > API**.`;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
// Fallback para respostas simuladas se não houver API key
|
|
287
|
+
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1000));
|
|
288
|
+
resposta = gerarResposta(mensagemUsuario);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const novaMensagem: Message = {
|
|
292
|
+
id: Date.now().toString() + '-ia',
|
|
293
|
+
type: 'assistant',
|
|
294
|
+
content: resposta,
|
|
295
|
+
timestamp: new Date(),
|
|
296
|
+
isFavorite: false
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
setConversas(prev => prev.map(conv => {
|
|
300
|
+
if (conv.id === conversaAtual) {
|
|
301
|
+
const novasMensagens = [...conv.mensagens, novaMensagem];
|
|
302
|
+
return {
|
|
303
|
+
...conv,
|
|
304
|
+
mensagens: novasMensagens,
|
|
305
|
+
ultimaMensagem: resposta.substring(0, 50) + '...',
|
|
306
|
+
timestamp: new Date().toLocaleString('pt-BR')
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return conv;
|
|
310
|
+
}));
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error('Erro ao gerar resposta:', error);
|
|
313
|
+
const mensagemErro: Message = {
|
|
314
|
+
id: Date.now().toString() + '-error',
|
|
315
|
+
type: 'assistant',
|
|
316
|
+
content: 'Desculpe, ocorreu um erro ao processar sua mensagem. Por favor, tente novamente.',
|
|
317
|
+
timestamp: new Date(),
|
|
318
|
+
isFavorite: false
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
setConversas(prev => prev.map(conv => {
|
|
322
|
+
if (conv.id === conversaAtual) {
|
|
323
|
+
return {
|
|
324
|
+
...conv,
|
|
325
|
+
mensagens: [...conv.mensagens, mensagemErro]
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
return conv;
|
|
329
|
+
}));
|
|
330
|
+
} finally {
|
|
331
|
+
setIsTyping(false);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const simularRespostaDocumento = (tema: string) => {
|
|
336
|
+
setIsTyping(true);
|
|
337
|
+
|
|
338
|
+
setTimeout(() => {
|
|
339
|
+
const temaLimpo = tema.replace(/📄 \[Criar documento\]/gi, '').trim();
|
|
340
|
+
const documentTitle = `Documento: ${temaLimpo}`;
|
|
341
|
+
const documentContent = `# ${temaLimpo}
|
|
342
|
+
|
|
343
|
+
## Introdução
|
|
344
|
+
|
|
345
|
+
Este documento foi gerado automaticamente pelo Assistente Xertica com base no tema "${temaLimpo}".
|
|
346
|
+
|
|
347
|
+
## Visão Geral
|
|
348
|
+
|
|
349
|
+
O tema aborda aspectos importantes que serão detalhados nas próximas seções. Este documento serve como um guia completo e pode ser editado conforme necessário.
|
|
350
|
+
|
|
351
|
+
## Análise Detalhada
|
|
352
|
+
|
|
353
|
+
### Contexto
|
|
354
|
+
|
|
355
|
+
Analisando o contexto relacionado a ${temaLimpo}, identificamos diversos pontos relevantes que merecem atenção especial.
|
|
356
|
+
|
|
357
|
+
### Principais Características
|
|
358
|
+
|
|
359
|
+
- **Aspecto 1**: Descrição detalhada do primeiro aspecto relevante
|
|
360
|
+
- **Aspecto 2**: Informações sobre o segundo ponto importante
|
|
361
|
+
- **Aspecto 3**: Análise do terceiro elemento fundamental
|
|
362
|
+
|
|
363
|
+
### Dados e Métricas
|
|
364
|
+
|
|
365
|
+
Os dados coletados indicam tendências significativas que devem ser consideradas no planejamento e execução das atividades relacionadas.
|
|
366
|
+
|
|
367
|
+
## Recomendações
|
|
368
|
+
|
|
369
|
+
Com base na análise realizada, recomendamos:
|
|
370
|
+
|
|
371
|
+
1. Implementar as melhores práticas identificadas
|
|
372
|
+
2. Monitorar continuamente os indicadores-chave
|
|
373
|
+
3. Ajustar estratégias conforme necessário
|
|
374
|
+
4. Documentar todos os processos e resultados
|
|
375
|
+
|
|
376
|
+
## Conclusão
|
|
377
|
+
|
|
378
|
+
Este documento fornece uma base sólida para entender e trabalhar com ${temaLimpo}. As informações aqui contidas devem ser revisadas e atualizadas regularmente.
|
|
379
|
+
|
|
380
|
+
## Próximos Passos
|
|
381
|
+
|
|
382
|
+
- [ ] Revisar e validar as informações
|
|
383
|
+
- [ ] Compartilhar com a equipe
|
|
384
|
+
- [ ] Implementar as recomendações
|
|
385
|
+
- [ ] Agendar revisão periódica
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
*Documento gerado por Xertica AI em ${new Date().toLocaleDateString('pt-BR')}*`;
|
|
390
|
+
|
|
391
|
+
const novaMensagemIA: Message = {
|
|
392
|
+
id: Date.now().toString(),
|
|
393
|
+
type: 'assistant',
|
|
394
|
+
content: `📝 Documento criado com sucesso!\n\nGerei um documento completo sobre "${temaLimpo}" com as seguintes seções:\n\n• Introdução e contexto\n• Análise detalhada\n• Dados e métricas relevantes\n• Recomendações\n• Conclusões\n• Próximos passos\n\nO documento está pronto para revisão e pode ser editado conforme necessário. Clique no botão "Editar" acima para abrir o editor completo.`,
|
|
395
|
+
timestamp: new Date(),
|
|
396
|
+
isFavorite: false,
|
|
397
|
+
attachmentType: 'document',
|
|
398
|
+
attachmentName: `${temaLimpo}.md`,
|
|
399
|
+
documentContent: documentContent,
|
|
400
|
+
documentTitle: documentTitle
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
setConversas(prev => prev.map(conv => {
|
|
404
|
+
if (conv.id === conversaAtual) {
|
|
405
|
+
return {
|
|
406
|
+
...conv,
|
|
407
|
+
mensagens: [...conv.mensagens, novaMensagemIA],
|
|
408
|
+
ultimaMensagem: 'Documento criado',
|
|
409
|
+
timestamp: new Date().toLocaleString('pt-BR')
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
return conv;
|
|
413
|
+
}));
|
|
414
|
+
|
|
415
|
+
setIsTyping(false);
|
|
416
|
+
}, 2000);
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const simularRespostaPodcast = (tema: string) => {
|
|
420
|
+
setIsTyping(true);
|
|
421
|
+
|
|
422
|
+
setTimeout(() => {
|
|
423
|
+
const temaLimpo = tema.replace(/🎙️ \[Gerar podcast\]/gi, '').trim();
|
|
424
|
+
|
|
425
|
+
// Gerar URL de áudio simulado (usando um data URL de áudio vazio)
|
|
426
|
+
// Em produção, isso seria substituído por uma chamada real à API de geração de áudio
|
|
427
|
+
const audioUrl = 'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA//tQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAADhAC7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7v///////////////////////////////////////////8AAAAATGF2YzU4LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAA4TnTxAOAAAAAAD/+xBkAA';
|
|
428
|
+
|
|
429
|
+
const novaMensagemIA: Message = {
|
|
430
|
+
id: Date.now().toString(),
|
|
431
|
+
type: 'assistant',
|
|
432
|
+
content: `🎙️ Podcast gerado com sucesso!\n\nCriei um podcast completo sobre "${temaLimpo}" com:\n\n• Introdução envolvente ao tema\n• Desenvolvimento detalhado do conteúdo\n• Exemplos práticos e aplicações\n• Reflexões e insights\n• Conclusão com próximos passos\n\nDuração estimada: 5-7 minutos\n\nAperte play abaixo para ouvir:`,
|
|
433
|
+
timestamp: new Date(),
|
|
434
|
+
isFavorite: false,
|
|
435
|
+
attachmentType: 'podcast',
|
|
436
|
+
attachmentName: `Podcast - ${temaLimpo}.mp3`,
|
|
437
|
+
audioUrl: audioUrl
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
setConversas(prev => prev.map(conv => {
|
|
441
|
+
if (conv.id === conversaAtual) {
|
|
442
|
+
return {
|
|
443
|
+
...conv,
|
|
444
|
+
mensagens: [...conv.mensagens, novaMensagemIA],
|
|
445
|
+
ultimaMensagem: 'Podcast gerado',
|
|
446
|
+
timestamp: new Date().toLocaleString('pt-BR')
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
return conv;
|
|
450
|
+
}));
|
|
451
|
+
|
|
452
|
+
setIsTyping(false);
|
|
453
|
+
}, 2000);
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const simularRespostaPesquisa = (query: string) => {
|
|
457
|
+
setIsTyping(true);
|
|
458
|
+
|
|
459
|
+
setTimeout(() => {
|
|
460
|
+
const queryLimpa = query.replace(/🔍 \[Pesquisar\]/gi, '').trim();
|
|
461
|
+
|
|
462
|
+
// Simular resultados de pesquisa baseados na query
|
|
463
|
+
const mockResults: SearchResult[] = [
|
|
464
|
+
{
|
|
465
|
+
id: '1',
|
|
466
|
+
title: `Projeto relacionado a ${queryLimpa}`,
|
|
467
|
+
description: 'Documento contendo informações detalhadas sobre o projeto e seus objetivos principais.',
|
|
468
|
+
type: 'project',
|
|
469
|
+
path: '/projetos/projeto-principal',
|
|
470
|
+
relevance: 95,
|
|
471
|
+
lastModified: '2 horas atrás'
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
id: '2',
|
|
475
|
+
title: `Análise de dados - ${queryLimpa}`,
|
|
476
|
+
description: 'Relatório completo com métricas, gráficos e insights sobre o desempenho.',
|
|
477
|
+
type: 'document',
|
|
478
|
+
path: '/documentos/analise-dados',
|
|
479
|
+
relevance: 87,
|
|
480
|
+
lastModified: '1 dia atrás'
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
id: '3',
|
|
484
|
+
title: `Conversas sobre ${queryLimpa}`,
|
|
485
|
+
description: 'Histórico de discussões e decisões tomadas pela equipe.',
|
|
486
|
+
type: 'conversation',
|
|
487
|
+
path: '/conversas/historico-2024',
|
|
488
|
+
relevance: 78,
|
|
489
|
+
lastModified: '3 dias atrás'
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
id: '4',
|
|
493
|
+
title: 'Apresentação executiva',
|
|
494
|
+
description: 'Slides e material de apresentação para stakeholders.',
|
|
495
|
+
type: 'file',
|
|
496
|
+
path: '/arquivos/apresentacao.pdf',
|
|
497
|
+
relevance: 72,
|
|
498
|
+
lastModified: '1 semana atrás'
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
id: '5',
|
|
502
|
+
title: 'Contatos da equipe',
|
|
503
|
+
description: 'Lista de membros envolvidos e suas responsabilidades.',
|
|
504
|
+
type: 'contact',
|
|
505
|
+
path: '/equipe/contatos',
|
|
506
|
+
relevance: 65,
|
|
507
|
+
lastModified: '2 semanas atrás'
|
|
508
|
+
}
|
|
509
|
+
];
|
|
510
|
+
|
|
511
|
+
const mockSources: SearchSource[] = [
|
|
512
|
+
{ name: 'Documentos', count: 12 },
|
|
513
|
+
{ name: 'Projetos', count: 8 },
|
|
514
|
+
{ name: 'Conversas', count: 15 },
|
|
515
|
+
{ name: 'Arquivos', count: 23 },
|
|
516
|
+
{ name: 'Contatos', count: 5 }
|
|
517
|
+
];
|
|
518
|
+
|
|
519
|
+
const mockCommands: SearchCommand[] = [
|
|
520
|
+
{
|
|
521
|
+
id: '1',
|
|
522
|
+
icon: '📄',
|
|
523
|
+
label: 'Criar documento',
|
|
524
|
+
description: 'Criar um novo documento com os resultados'
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
id: '2',
|
|
528
|
+
icon: '📊',
|
|
529
|
+
label: 'Gerar relatório',
|
|
530
|
+
description: 'Compilar informações em um relatório'
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
id: '3',
|
|
534
|
+
icon: '🎙️',
|
|
535
|
+
label: 'Gerar podcast',
|
|
536
|
+
description: 'Criar podcast resumindo os achados'
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
id: '4',
|
|
540
|
+
icon: '📧',
|
|
541
|
+
label: 'Enviar resumo',
|
|
542
|
+
description: 'Enviar resumo por e-mail para a equipe'
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
id: '5',
|
|
546
|
+
icon: '⭐',
|
|
547
|
+
label: 'Salvar pesquisa',
|
|
548
|
+
description: 'Salvar esta pesquisa nos favoritos'
|
|
549
|
+
}
|
|
550
|
+
];
|
|
551
|
+
|
|
552
|
+
const novaMensagemIA: Message = {
|
|
553
|
+
id: Date.now().toString(),
|
|
554
|
+
type: 'assistant',
|
|
555
|
+
content: `🔍 Pesquisa realizada com sucesso!\n\nEncontrei ${mockResults.length} resultados relevantes sobre "${queryLimpa}" em ${mockSources.reduce((acc, s) => acc + s.count, 0)} fontes diferentes.\n\nOs resultados estão organizados por relevância e incluem documentos, projetos, conversas e arquivos relacionados.`,
|
|
556
|
+
timestamp: new Date(),
|
|
557
|
+
isFavorite: false,
|
|
558
|
+
attachmentType: 'search',
|
|
559
|
+
searchResults: mockResults,
|
|
560
|
+
searchSources: mockSources,
|
|
561
|
+
searchCommands: mockCommands
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
setConversas(prev => prev.map(conv => {
|
|
565
|
+
if (conv.id === conversaAtual) {
|
|
566
|
+
return {
|
|
567
|
+
...conv,
|
|
568
|
+
mensagens: [...conv.mensagens, novaMensagemIA],
|
|
569
|
+
ultimaMensagem: 'Pesquisa realizada',
|
|
570
|
+
timestamp: new Date().toLocaleString('pt-BR')
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
return conv;
|
|
574
|
+
}));
|
|
575
|
+
|
|
576
|
+
setIsTyping(false);
|
|
577
|
+
}, 1500);
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// Detectar intenção do usuário baseado na mensagem com análise contextual avançada
|
|
581
|
+
const detectarIntencao = (texto: string): ActionType | null => {
|
|
582
|
+
const textoLower = texto.toLowerCase().trim();
|
|
583
|
+
|
|
584
|
+
// Normalizar texto removendo pontuação extra
|
|
585
|
+
const textoNormalizado = textoLower.replace(/[?.!,]/g, ' ').replace(/\s+/g, ' ');
|
|
586
|
+
|
|
587
|
+
// ====== VERBOS DE AÇÃO PARA DOCUMENTO ======
|
|
588
|
+
const verbosDocumento = [
|
|
589
|
+
'crie', 'criar', 'criação', 'cria',
|
|
590
|
+
'faça', 'fazer', 'faz', 'fazendo',
|
|
591
|
+
'gere', 'gerar', 'gera', 'gerando', 'geração',
|
|
592
|
+
'escreva', 'escrever', 'escreve', 'escrevendo',
|
|
593
|
+
'monte', 'montar', 'monta', 'montando',
|
|
594
|
+
'prepare', 'preparar', 'prepara', 'preparando', 'preparação',
|
|
595
|
+
'elabore', 'elaborar', 'elabora', 'elaborando', 'elaboração',
|
|
596
|
+
'redija', 'redigir', 'redige', 'redigindo',
|
|
597
|
+
'produza', 'produzir', 'produz', 'produzindo', 'produção',
|
|
598
|
+
'compile', 'compilar', 'compila', 'compilando', 'compilação',
|
|
599
|
+
'desenvolva', 'desenvolver', 'desenvolve', 'desenvolvendo',
|
|
600
|
+
'formule', 'formular', 'formula', 'formulando'
|
|
601
|
+
];
|
|
602
|
+
|
|
603
|
+
const substantivosDocumento = [
|
|
604
|
+
'documento', 'documentos', 'doc',
|
|
605
|
+
'relatório', 'relatorio', 'relatórios', 'relatorios',
|
|
606
|
+
'resumo', 'resumos',
|
|
607
|
+
'nota', 'notas', 'anotação', 'anotações', 'anotacoes',
|
|
608
|
+
'artigo', 'artigos',
|
|
609
|
+
'texto', 'textos',
|
|
610
|
+
'conteúdo', 'conteudo',
|
|
611
|
+
'análise', 'analise', 'análises', 'analises',
|
|
612
|
+
'estudo', 'estudos',
|
|
613
|
+
'resenha', 'resenhas',
|
|
614
|
+
'apresentação', 'apresentacao', 'apresentações', 'apresentacoes',
|
|
615
|
+
'memorial', 'memorando',
|
|
616
|
+
'ata', 'atas',
|
|
617
|
+
'minuta', 'minutas',
|
|
618
|
+
'proposta', 'propostas',
|
|
619
|
+
'briefing', 'brief'
|
|
620
|
+
];
|
|
621
|
+
|
|
622
|
+
// ====== VERBOS DE AÇÃO PARA PODCAST ======
|
|
623
|
+
const verbosPodcast = [
|
|
624
|
+
'grave', 'gravar', 'grava', 'gravando', 'gravação', 'gravacao',
|
|
625
|
+
'narre', 'narrar', 'narra', 'narrando', 'narração', 'narracao',
|
|
626
|
+
'transforme em áudio', 'transforme em audio',
|
|
627
|
+
'converta para áudio', 'converta para audio',
|
|
628
|
+
'sintetize', 'sintetizar'
|
|
629
|
+
];
|
|
630
|
+
|
|
631
|
+
const substantivosPodcast = [
|
|
632
|
+
'podcast', 'podcasts',
|
|
633
|
+
'áudio', 'audio', 'áudios', 'audios',
|
|
634
|
+
'episódio', 'episodio', 'episódios', 'episodios',
|
|
635
|
+
'narração', 'narracao', 'narrações', 'narracoes',
|
|
636
|
+
'locução', 'locucao',
|
|
637
|
+
'gravação', 'gravacao', 'gravações', 'gravacoes'
|
|
638
|
+
];
|
|
639
|
+
|
|
640
|
+
// ====== VERBOS DE AÇÃO PARA PESQUISA ======
|
|
641
|
+
const verbosPesquisa = [
|
|
642
|
+
'pesquise', 'pesquisar', 'pesquisa', 'pesquisando',
|
|
643
|
+
'busque', 'buscar', 'busca', 'buscando',
|
|
644
|
+
'procure', 'procurar', 'procura', 'procurando',
|
|
645
|
+
'encontre', 'encontrar', 'encontra', 'encontrando',
|
|
646
|
+
'ache', 'achar', 'acha', 'achando',
|
|
647
|
+
'localize', 'localizar', 'localiza', 'localizando',
|
|
648
|
+
'consulte', 'consultar', 'consulta', 'consultando',
|
|
649
|
+
'veja', 'ver', 'vê',
|
|
650
|
+
'mostre', 'mostrar', 'mostra', 'mostrando',
|
|
651
|
+
'exiba', 'exibir', 'exibe', 'exibindo',
|
|
652
|
+
'liste', 'listar', 'lista', 'listando',
|
|
653
|
+
'traga', 'trazer', 'traz', 'trazendo',
|
|
654
|
+
'recupere', 'recuperar', 'recupera', 'recuperando'
|
|
655
|
+
];
|
|
656
|
+
|
|
657
|
+
const complementosPesquisa = [
|
|
658
|
+
'me mostre', 'mostre-me', 'me mostra',
|
|
659
|
+
'me traga', 'traga-me', 'me traz',
|
|
660
|
+
'quero ver', 'quero saber', 'quero encontrar',
|
|
661
|
+
'preciso de', 'preciso ver', 'preciso encontrar',
|
|
662
|
+
'gostaria de ver', 'gostaria de saber',
|
|
663
|
+
'pode me mostrar', 'pode me trazer',
|
|
664
|
+
'você tem', 'tem algum', 'tem alguma',
|
|
665
|
+
'existe', 'existem', 'há algum', 'há alguma',
|
|
666
|
+
'onde está', 'onde estão', 'onde fica', 'onde ficam',
|
|
667
|
+
'qual é', 'quais são'
|
|
668
|
+
];
|
|
669
|
+
|
|
670
|
+
// ====== DETECÇÃO CONTEXTUAL ======
|
|
671
|
+
|
|
672
|
+
// 1. Verificar PODCAST primeiro (mais específico)
|
|
673
|
+
for (const verbo of verbosPodcast) {
|
|
674
|
+
if (textoNormalizado.includes(verbo)) {
|
|
675
|
+
return 'podcast';
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
for (const substantivo of substantivosPodcast) {
|
|
680
|
+
for (const verbo of verbosDocumento) {
|
|
681
|
+
if (textoNormalizado.includes(verbo) && textoNormalizado.includes(substantivo)) {
|
|
682
|
+
return 'podcast';
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Detecção específica: "transformar/converter em áudio"
|
|
688
|
+
if ((textoNormalizado.includes('transforme') || textoNormalizado.includes('transformar') ||
|
|
689
|
+
textoNormalizado.includes('converta') || textoNormalizado.includes('converter')) &&
|
|
690
|
+
(textoNormalizado.includes('áudio') || textoNormalizado.includes('audio'))) {
|
|
691
|
+
return 'podcast';
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// 2. Verificar DOCUMENTO (combinação verbo + substantivo)
|
|
695
|
+
for (const verbo of verbosDocumento) {
|
|
696
|
+
for (const substantivo of substantivosDocumento) {
|
|
697
|
+
if (textoNormalizado.includes(verbo) && textoNormalizado.includes(substantivo)) {
|
|
698
|
+
return 'document';
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Detecção simples: apenas o verbo "documentar"
|
|
704
|
+
if (textoNormalizado.includes('documente') || textoNormalizado.includes('documentar')) {
|
|
705
|
+
return 'document';
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Detecção: "escrever/escreva sobre"
|
|
709
|
+
if ((textoNormalizado.includes('escreva sobre') || textoNormalizado.includes('escrever sobre') ||
|
|
710
|
+
textoNormalizado.includes('escreva um') || textoNormalizado.includes('escreva uma'))) {
|
|
711
|
+
return 'document';
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Padrões comuns de criação de documento
|
|
715
|
+
const padrõesDocumento = [
|
|
716
|
+
/^(crie|criar|faça|fazer|gere|gerar).*(documento|relatório|relatorio|resumo|nota|artigo|texto|análise|analise)/,
|
|
717
|
+
/^(preciso|quero|gostaria).*(criar|fazer|gerar).*(documento|relatório|relatorio|resumo)/,
|
|
718
|
+
/^me (ajude|ajuda) (a )?(criar|fazer|gerar).*(documento|relatório|relatorio)/
|
|
719
|
+
];
|
|
720
|
+
|
|
721
|
+
for (const padrao of padrõesDocumento) {
|
|
722
|
+
if (padrao.test(textoNormalizado)) {
|
|
723
|
+
return 'document';
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// 3. Verificar PESQUISA
|
|
728
|
+
for (const verbo of verbosPesquisa) {
|
|
729
|
+
if (textoNormalizado.startsWith(verbo + ' ') || textoNormalizado.includes(' ' + verbo + ' ')) {
|
|
730
|
+
return 'search';
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
for (const complemento of complementosPesquisa) {
|
|
735
|
+
if (textoNormalizado.includes(complemento)) {
|
|
736
|
+
return 'search';
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Padrões interrogativos que indicam pesquisa
|
|
741
|
+
if ((textoNormalizado.startsWith('onde ') || textoNormalizado.startsWith('qual ') ||
|
|
742
|
+
textoNormalizado.startsWith('quais ') || textoNormalizado.startsWith('quem ') ||
|
|
743
|
+
textoNormalizado.startsWith('como encontrar') || textoNormalizado.startsWith('tem algum') ||
|
|
744
|
+
textoNormalizado.startsWith('tem alguma') || textoNormalizado.includes('você tem')) &&
|
|
745
|
+
!textoNormalizado.includes('documento') && !textoNormalizado.includes('relatório') &&
|
|
746
|
+
!textoNormalizado.includes('relatorio') && !textoNormalizado.includes('podcast')) {
|
|
747
|
+
return 'search';
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return null;
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
const handleEnviarMensagem = (action?: ActionType) => {
|
|
754
|
+
if (mensagem.trim()) {
|
|
755
|
+
// Se não houver ação explícita, tentar detectar a intenção
|
|
756
|
+
let acaoDetectada = action;
|
|
757
|
+
if (!acaoDetectada) {
|
|
758
|
+
acaoDetectada = detectarIntencao(mensagem) || undefined;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
let mensagemComAcao = mensagem;
|
|
762
|
+
|
|
763
|
+
// Adicionar prefixo baseado na ação selecionada ou detectada
|
|
764
|
+
if (acaoDetectada === 'document') {
|
|
765
|
+
mensagemComAcao = `📄 [Criar documento] ${mensagem}`;
|
|
766
|
+
} else if (acaoDetectada === 'podcast') {
|
|
767
|
+
mensagemComAcao = `🎙️ [Gerar podcast] ${mensagem}`;
|
|
768
|
+
} else if (acaoDetectada === 'search') {
|
|
769
|
+
mensagemComAcao = `🔍 [Pesquisar] ${mensagem}`;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const novaMensagem: Message = {
|
|
773
|
+
id: Date.now().toString(),
|
|
774
|
+
type: 'user',
|
|
775
|
+
content: mensagemComAcao,
|
|
776
|
+
timestamp: new Date(),
|
|
777
|
+
isFavorite: false
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
// Atualizar título da conversa se for a primeira mensagem
|
|
781
|
+
setConversas(prev => prev.map(conv => {
|
|
782
|
+
if (conv.id === conversaAtual) {
|
|
783
|
+
const novasMensagens = [...conv.mensagens, novaMensagem];
|
|
784
|
+
const novoTitulo = conv.mensagens.length === 0
|
|
785
|
+
? mensagem.substring(0, 30) + (mensagem.length > 30 ? '...' : '')
|
|
786
|
+
: conv.titulo;
|
|
787
|
+
|
|
788
|
+
return {
|
|
789
|
+
...conv,
|
|
790
|
+
titulo: novoTitulo,
|
|
791
|
+
mensagens: novasMensagens,
|
|
792
|
+
ultimaMensagem: mensagem,
|
|
793
|
+
timestamp: new Date().toLocaleString('pt-BR')
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
return conv;
|
|
797
|
+
}));
|
|
798
|
+
|
|
799
|
+
setMensagem('');
|
|
800
|
+
|
|
801
|
+
// Simular resposta da IA baseada na ação detectada ou explícita
|
|
802
|
+
if (acaoDetectada === 'document') {
|
|
803
|
+
simularRespostaDocumento(mensagem);
|
|
804
|
+
} else if (acaoDetectada === 'podcast') {
|
|
805
|
+
simularRespostaPodcast(mensagem);
|
|
806
|
+
} else if (acaoDetectada === 'search') {
|
|
807
|
+
simularRespostaPesquisa(mensagem);
|
|
808
|
+
} else {
|
|
809
|
+
simularRespostaIA(mensagem);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
const handleToggleFavorite = (messageId: string) => {
|
|
815
|
+
setConversas(prev => prev.map(conv => {
|
|
816
|
+
if (conv.id === conversaAtual) {
|
|
817
|
+
return {
|
|
818
|
+
...conv,
|
|
819
|
+
mensagens: conv.mensagens.map(msg =>
|
|
820
|
+
msg.id === messageId ? { ...msg, isFavorite: !msg.isFavorite } : msg
|
|
821
|
+
)
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
return conv;
|
|
825
|
+
}));
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
const handleCopyMessage = async (content: string, messageId: string) => {
|
|
829
|
+
try {
|
|
830
|
+
// Try modern Clipboard API first
|
|
831
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
832
|
+
try {
|
|
833
|
+
await navigator.clipboard.writeText(content);
|
|
834
|
+
setCopiedId(messageId);
|
|
835
|
+
setTimeout(() => setCopiedId(null), 2000);
|
|
836
|
+
} catch (clipboardError: any) {
|
|
837
|
+
// If clipboard API fails due to permissions, fall back to execCommand
|
|
838
|
+
if (clipboardError.name === 'NotAllowedError' || clipboardError.message.includes('permissions policy')) {
|
|
839
|
+
throw new Error('Clipboard permission denied, falling back');
|
|
840
|
+
}
|
|
841
|
+
throw clipboardError;
|
|
842
|
+
}
|
|
843
|
+
} else {
|
|
844
|
+
throw new Error('Clipboard API not available');
|
|
845
|
+
}
|
|
846
|
+
} catch (err) {
|
|
847
|
+
// Fallback for browsers that don't support Clipboard API or when permissions are denied
|
|
848
|
+
try {
|
|
849
|
+
const textArea = document.createElement('textarea');
|
|
850
|
+
textArea.value = content;
|
|
851
|
+
textArea.style.position = 'fixed';
|
|
852
|
+
textArea.style.left = '-999999px';
|
|
853
|
+
textArea.style.top = '-999999px';
|
|
854
|
+
document.body.appendChild(textArea);
|
|
855
|
+
textArea.focus();
|
|
856
|
+
textArea.select();
|
|
857
|
+
|
|
858
|
+
const successful = document.execCommand('copy');
|
|
859
|
+
document.body.removeChild(textArea);
|
|
860
|
+
|
|
861
|
+
if (successful) {
|
|
862
|
+
setCopiedId(messageId);
|
|
863
|
+
setTimeout(() => setCopiedId(null), 2000);
|
|
864
|
+
} else {
|
|
865
|
+
console.error('Falha ao copiar: execCommand returned false');
|
|
866
|
+
}
|
|
867
|
+
} catch (fallbackErr) {
|
|
868
|
+
console.error('Falha ao copiar:', fallbackErr);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
const handleToggleFavoritaConversa = (conversaId: string) => {
|
|
874
|
+
setConversas(prev => prev.map(conv =>
|
|
875
|
+
conv.id === conversaId ? { ...conv, favorita: !conv.favorita } : conv
|
|
876
|
+
));
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
const handleNovaConversa = () => {
|
|
880
|
+
const novaConversa: Conversa = {
|
|
881
|
+
id: Date.now().toString(),
|
|
882
|
+
titulo: 'Nova Conversa',
|
|
883
|
+
mensagens: [],
|
|
884
|
+
ultimaMensagem: '',
|
|
885
|
+
timestamp: new Date().toLocaleString('pt-BR'),
|
|
886
|
+
favorita: false
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
setConversas(prev => [novaConversa, ...prev]);
|
|
890
|
+
setConversaAtual(novaConversa.id);
|
|
891
|
+
setAbaSelecionada('chat');
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
const handleSelecionarConversa = (conversaId: string) => {
|
|
895
|
+
setConversaAtual(conversaId);
|
|
896
|
+
setAbaSelecionada('chat');
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
900
|
+
const file = e.target.files?.[0];
|
|
901
|
+
if (file) {
|
|
902
|
+
const novaMensagem: Message = {
|
|
903
|
+
id: Date.now().toString(),
|
|
904
|
+
type: 'user',
|
|
905
|
+
content: `Enviei o arquivo: ${file.name}`,
|
|
906
|
+
timestamp: new Date(),
|
|
907
|
+
isFavorite: false,
|
|
908
|
+
attachmentType: 'file',
|
|
909
|
+
attachmentName: file.name
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
setConversas(prev => prev.map(conv => {
|
|
913
|
+
if (conv.id === conversaAtual) {
|
|
914
|
+
return {
|
|
915
|
+
...conv,
|
|
916
|
+
mensagens: [...conv.mensagens, novaMensagem],
|
|
917
|
+
ultimaMensagem: `Arquivo: ${file.name}`,
|
|
918
|
+
timestamp: new Date().toLocaleString('pt-BR')
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
return conv;
|
|
922
|
+
}));
|
|
923
|
+
|
|
924
|
+
// Simular resposta sobre o arquivo
|
|
925
|
+
setTimeout(() => {
|
|
926
|
+
simularRespostaIA(`Analisar arquivo ${file.name}`);
|
|
927
|
+
}, 500);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Reset input
|
|
931
|
+
if (e.target) e.target.value = '';
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
const handleAudioUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
935
|
+
const file = e.target.files?.[0];
|
|
936
|
+
if (file) {
|
|
937
|
+
const novaMensagem: Message = {
|
|
938
|
+
id: Date.now().toString(),
|
|
939
|
+
type: 'user',
|
|
940
|
+
content: `Enviei um áudio: ${file.name}`,
|
|
941
|
+
timestamp: new Date(),
|
|
942
|
+
isFavorite: false,
|
|
943
|
+
attachmentType: 'audio',
|
|
944
|
+
attachmentName: file.name
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
setConversas(prev => prev.map(conv => {
|
|
948
|
+
if (conv.id === conversaAtual) {
|
|
949
|
+
return {
|
|
950
|
+
...conv,
|
|
951
|
+
mensagens: [...conv.mensagens, novaMensagem],
|
|
952
|
+
ultimaMensagem: `Áudio: ${file.name}`,
|
|
953
|
+
timestamp: new Date().toLocaleString('pt-BR')
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
return conv;
|
|
957
|
+
}));
|
|
958
|
+
|
|
959
|
+
// Simular resposta sobre o áudio
|
|
960
|
+
setTimeout(() => {
|
|
961
|
+
const respostaAudio = 'Áudio recebido! Estou processando o conteúdo... 🎵\n\nIdentifiquei que você está falando sobre análise de projetos. Posso transcrever o áudio completo se desejar. Gostaria que eu faça isso?';
|
|
962
|
+
|
|
963
|
+
const novaMensagemIA: Message = {
|
|
964
|
+
id: Date.now().toString() + '-ia',
|
|
965
|
+
type: 'assistant',
|
|
966
|
+
content: respostaAudio,
|
|
967
|
+
timestamp: new Date(),
|
|
968
|
+
isFavorite: false
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
setConversas(prev => prev.map(conv => {
|
|
972
|
+
if (conv.id === conversaAtual) {
|
|
973
|
+
return {
|
|
974
|
+
...conv,
|
|
975
|
+
mensagens: [...conv.mensagens, novaMensagemIA],
|
|
976
|
+
ultimaMensagem: respostaAudio.substring(0, 50) + '...',
|
|
977
|
+
timestamp: new Date().toLocaleString('pt-BR')
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
return conv;
|
|
981
|
+
}));
|
|
982
|
+
}, 1500);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Reset input
|
|
986
|
+
if (e.target) e.target.value = '';
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
const handleGeneratePodcast = async (messageId: string, messageContent: string) => {
|
|
990
|
+
// Marcar que o podcast está sendo gerado
|
|
991
|
+
setGeneratingPodcastId(messageId);
|
|
992
|
+
|
|
993
|
+
// Simular delay de geração (2-3 segundos)
|
|
994
|
+
await new Promise(resolve => setTimeout(resolve, 2000 + Math.random() * 1000));
|
|
995
|
+
|
|
996
|
+
// Gerar URL de áudio simulado (usando um data URL de áudio vazio)
|
|
997
|
+
// Em produção, isso seria substituído por uma chamada real à API de geração de áudio
|
|
998
|
+
const audioUrl = 'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA//tQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAADhAC7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7v///////////////////////////////////////////8AAAAATGF2YzU4LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAA4TnTxAOAAAAAAD/+xBkAA';
|
|
999
|
+
|
|
1000
|
+
const temaExtraido = messageContent.substring(0, 50).replace(/[📝🎙️🔍]/g, '').trim();
|
|
1001
|
+
|
|
1002
|
+
const podcastMessage: Message = {
|
|
1003
|
+
id: Date.now().toString() + '-podcast',
|
|
1004
|
+
type: 'assistant',
|
|
1005
|
+
content: `🎙️ Podcast gerado com sucesso!\n\nCriei um podcast baseado na mensagem anterior com:\n\n• Introdução envolvente ao tema\n• Desenvolvimento detalhado do conteúdo\n• Exemplos práticos e aplicações\n• Reflexões e insights\n• Conclusão com próximos passos\n\nDuração estimada: 5-7 minutos\n\nAperte play abaixo para ouvir:`,
|
|
1006
|
+
timestamp: new Date(),
|
|
1007
|
+
isFavorite: false,
|
|
1008
|
+
attachmentType: 'podcast',
|
|
1009
|
+
attachmentName: `Podcast - ${temaExtraido}.mp3`,
|
|
1010
|
+
audioUrl: audioUrl
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
setConversas(prev => prev.map(conv => {
|
|
1014
|
+
if (conv.id === conversaAtual) {
|
|
1015
|
+
return {
|
|
1016
|
+
...conv,
|
|
1017
|
+
mensagens: [...conv.mensagens, podcastMessage],
|
|
1018
|
+
ultimaMensagem: 'Podcast gerado',
|
|
1019
|
+
timestamp: new Date().toLocaleString('pt-BR')
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
return conv;
|
|
1023
|
+
}));
|
|
1024
|
+
|
|
1025
|
+
setGeneratingPodcastId(null);
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
const conversasFiltradas = abaSelecionada === 'favoritos'
|
|
1029
|
+
? conversas.filter(c => c.favorita)
|
|
1030
|
+
: conversas;
|
|
1031
|
+
|
|
1032
|
+
const handleExpandWithTab = (tab: 'chat' | 'historico' | 'favoritos') => {
|
|
1033
|
+
setAbaSelecionada(tab);
|
|
1034
|
+
if (onToggleWithTab) {
|
|
1035
|
+
onToggleWithTab(tab);
|
|
1036
|
+
} else if (!isExpanded) {
|
|
1037
|
+
onToggle();
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
const handleEditDocument = (content: string, title: string) => {
|
|
1042
|
+
setEditingDocument({ content, title });
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
const handleCloseEditor = () => {
|
|
1046
|
+
setEditingDocument(null);
|
|
1047
|
+
};
|
|
1048
|
+
|
|
1049
|
+
const handleDownloadDocument = (content: string, filename: string) => {
|
|
1050
|
+
const blob = new Blob([content], { type: 'text/markdown' });
|
|
1051
|
+
const url = URL.createObjectURL(blob);
|
|
1052
|
+
const a = document.createElement('a');
|
|
1053
|
+
a.href = url;
|
|
1054
|
+
a.download = filename;
|
|
1055
|
+
document.body.appendChild(a);
|
|
1056
|
+
a.click();
|
|
1057
|
+
document.body.removeChild(a);
|
|
1058
|
+
URL.revokeObjectURL(url);
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
const handleDownloadPodcast = (audioUrl: string, filename: string) => {
|
|
1062
|
+
const a = document.createElement('a');
|
|
1063
|
+
a.href = audioUrl;
|
|
1064
|
+
a.download = filename;
|
|
1065
|
+
document.body.appendChild(a);
|
|
1066
|
+
a.click();
|
|
1067
|
+
document.body.removeChild(a);
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
// Funções para ações de pesquisa
|
|
1071
|
+
const handleOpenSearchResult = (result: SearchResult) => {
|
|
1072
|
+
// Adicionar mensagem do usuário simulando clique
|
|
1073
|
+
const userMessage: Message = {
|
|
1074
|
+
id: Date.now().toString(),
|
|
1075
|
+
type: 'user',
|
|
1076
|
+
content: `Abrir: ${result.title}`,
|
|
1077
|
+
timestamp: new Date(),
|
|
1078
|
+
isFavorite: false
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
setConversas(prev => prev.map(conv => {
|
|
1082
|
+
if (conv.id === conversaAtual) {
|
|
1083
|
+
return {
|
|
1084
|
+
...conv,
|
|
1085
|
+
mensagens: [...conv.mensagens, userMessage],
|
|
1086
|
+
ultimaMensagem: userMessage.content,
|
|
1087
|
+
timestamp: new Date().toLocaleString('pt-BR')
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
return conv;
|
|
1091
|
+
}));
|
|
1092
|
+
|
|
1093
|
+
// Resposta do assistente com opções baseadas no tipo
|
|
1094
|
+
setTimeout(() => {
|
|
1095
|
+
let responseContent = '';
|
|
1096
|
+
let actionButtons = '';
|
|
1097
|
+
|
|
1098
|
+
if (result.type === 'document') {
|
|
1099
|
+
responseContent = `📄 **${result.title}**\n\n${result.description}\n\n**Localização:** ${result.path}\n${result.lastModified ? `**Última modificação:** ${result.lastModified}\n` : ''}\n**Relevância:** ${result.relevance}%\n\n**O que você gostaria de fazer?**\n\n• Visualizar conteúdo completo\n• Editar documento\n• Baixar arquivo\n• Compartilhar com equipe\n• Ver histórico de versões\n• Adicionar comentários`;
|
|
1100
|
+
} else if (result.type === 'project') {
|
|
1101
|
+
responseContent = `📁 **${result.title}**\n\n${result.description}\n\n**Localização:** ${result.path}\n${result.lastModified ? `**Última modificação:** ${result.lastModified}\n` : ''}\n**Relevância:** ${result.relevance}%\n\n**Opções disponíveis:**\n\n• Ver dashboard do projeto\n• Listar tarefas ativas\n• Ver membros da equipe\n• Gerar relatório de progresso\n• Acessar documentação\n• Visualizar timeline`;
|
|
1102
|
+
} else if (result.type === 'conversation') {
|
|
1103
|
+
responseContent = `💬 **${result.title}**\n\n${result.description}\n\n**Localização:** ${result.path}\n${result.lastModified ? `**Última interação:** ${result.lastModified}\n` : ''}\n**Relevância:** ${result.relevance}%\n\n**Ações disponíveis:**\n\n• Abrir conversa completa\n• Ver participantes\n• Exportar histórico\n• Buscar em mensagens\n• Marcar como importante\n• Criar resumo da conversa`;
|
|
1104
|
+
} else if (result.type === 'file') {
|
|
1105
|
+
responseContent = `📎 **${result.title}**\n\n${result.description}\n\n**Localização:** ${result.path}\n${result.lastModified ? `**Última modificação:** ${result.lastModified}\n` : ''}\n**Relevância:** ${result.relevance}%\n\n**O que posso fazer:**\n\n• Fazer download do arquivo\n• Visualizar prévia\n• Compartilhar link\n• Ver detalhes do arquivo\n• Mover para outra pasta\n• Renomear arquivo`;
|
|
1106
|
+
} else if (result.type === 'contact') {
|
|
1107
|
+
responseContent = `👤 **${result.title}**\n\n${result.description}\n\n**Localização:** ${result.path}\n${result.lastModified ? `**Última interação:** ${result.lastModified}\n` : ''}\n**Relevância:** ${result.relevance}%\n\n**Opções de contato:**\n\n• Ver perfil completo\n• Enviar mensagem\n• Agendar reunião\n• Ver projetos em comum\n• Histórico de interações\n• Adicionar nota`;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const assistantMessage: Message = {
|
|
1111
|
+
id: Date.now().toString() + '-response',
|
|
1112
|
+
type: 'assistant',
|
|
1113
|
+
content: responseContent,
|
|
1114
|
+
timestamp: new Date(),
|
|
1115
|
+
isFavorite: false
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
setConversas(prev => prev.map(conv => {
|
|
1119
|
+
if (conv.id === conversaAtual) {
|
|
1120
|
+
return {
|
|
1121
|
+
...conv,
|
|
1122
|
+
mensagens: [...conv.mensagens, assistantMessage],
|
|
1123
|
+
ultimaMensagem: 'Opções de ação',
|
|
1124
|
+
timestamp: new Date().toLocaleString('pt-BR')
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
return conv;
|
|
1128
|
+
}));
|
|
1129
|
+
}, 500);
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
const handleCreateDocumentFromSearch = (searchQuery: string, results: SearchResult[]) => {
|
|
1133
|
+
setExecutingCommand('document');
|
|
1134
|
+
|
|
1135
|
+
// Criar conteúdo do documento baseado nos resultados
|
|
1136
|
+
const documentContent = `# Relatório: ${searchQuery}\n\n## Resumo Executivo\n\nEsta pesquisa encontrou ${results.length} resultados relevantes sobre "${searchQuery}".\n\n## Resultados Encontrados\n\n${results.map((r, i) => `### ${i + 1}. ${r.title}\n- **Tipo:** ${r.type}\n- **Relevância:** ${r.relevance}%\n- **Descrição:** ${r.description}\n- **Localização:** ${r.path}\n${r.lastModified ? `- **Última modificação:** ${r.lastModified}` : ''}\n`).join('\n')}\n\n## Conclusão\n\nOs resultados acima representam as informações mais relevantes encontradas no sistema Xertica.ai.`;
|
|
1137
|
+
|
|
1138
|
+
setTimeout(() => {
|
|
1139
|
+
setExecutingCommand(null);
|
|
1140
|
+
simularRespostaDocumento(`Compilar resultados da pesquisa: ${searchQuery}`);
|
|
1141
|
+
}, 1500);
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
const handleGenerateReport = (searchQuery: string, results: SearchResult[]) => {
|
|
1145
|
+
setExecutingCommand('report');
|
|
1146
|
+
|
|
1147
|
+
setTimeout(() => {
|
|
1148
|
+
const reportMessage: Message = {
|
|
1149
|
+
id: Date.now().toString(),
|
|
1150
|
+
type: 'assistant',
|
|
1151
|
+
content: `📊 Relatório Analítico gerado!\n\n**Pesquisa:** ${searchQuery}\n\n**Métricas:**\n• ${results.length} resultados encontrados\n• Relevância média: ${Math.round(results.reduce((acc, r) => acc + r.relevance, 0) / results.length)}%\n• Tipos de conteúdo: ${new Set(results.map(r => r.type)).size}\n\n**Distribuição por tipo:**\n${Object.entries(results.reduce((acc, r) => ({ ...acc, [r.type]: (acc[r.type] || 0) + 1 }), {} as Record<string, number>)).map(([type, count]) => `• ${type}: ${count}`).join('\n')}\n\n**Recomenda��ões:**\n• Revisar os 3 resultados com maior relevância\n• Atualizar documentos com mais de 1 semana\n• Consolidar informações similares`,
|
|
1152
|
+
timestamp: new Date(),
|
|
1153
|
+
isFavorite: false
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
setConversas(prev => prev.map(conv => {
|
|
1157
|
+
if (conv.id === conversaAtual) {
|
|
1158
|
+
return {
|
|
1159
|
+
...conv,
|
|
1160
|
+
mensagens: [...conv.mensagens, reportMessage],
|
|
1161
|
+
ultimaMensagem: 'Relatório gerado',
|
|
1162
|
+
timestamp: new Date().toLocaleString('pt-BR')
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
return conv;
|
|
1166
|
+
}));
|
|
1167
|
+
|
|
1168
|
+
setExecutingCommand(null);
|
|
1169
|
+
}, 2000);
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1172
|
+
const handleGeneratePodcastFromSearch = (searchQuery: string) => {
|
|
1173
|
+
setExecutingCommand('podcast');
|
|
1174
|
+
|
|
1175
|
+
setTimeout(() => {
|
|
1176
|
+
simularRespostaPodcast(`Criar podcast sobre os resultados da pesquisa: ${searchQuery}`);
|
|
1177
|
+
setExecutingCommand(null);
|
|
1178
|
+
}, 1000);
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
const handleSendSummary = (searchQuery: string, results: SearchResult[]) => {
|
|
1182
|
+
setExecutingCommand('email');
|
|
1183
|
+
|
|
1184
|
+
setTimeout(() => {
|
|
1185
|
+
toast.success('Resumo enviado!', {
|
|
1186
|
+
description: `Email enviado para sua equipe com ${results.length} resultados sobre "${searchQuery}"`
|
|
1187
|
+
});
|
|
1188
|
+
setExecutingCommand(null);
|
|
1189
|
+
}, 1500);
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
const handleSaveSearch = (searchQuery: string, messageId: string) => {
|
|
1193
|
+
if (savedSearches.includes(messageId)) {
|
|
1194
|
+
setSavedSearches(prev => prev.filter(id => id !== messageId));
|
|
1195
|
+
toast.info('Pesquisa removida dos favoritos');
|
|
1196
|
+
} else {
|
|
1197
|
+
setSavedSearches(prev => [...prev, messageId]);
|
|
1198
|
+
toast.success('Pesquisa salva nos favoritos!', {
|
|
1199
|
+
description: `"${searchQuery}" foi adicionada aos seus favoritos`
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
const handleExecuteSearchCommand = (commandId: string, searchQuery: string, results: SearchResult[], messageId: string) => {
|
|
1205
|
+
switch (commandId) {
|
|
1206
|
+
case '1':
|
|
1207
|
+
handleCreateDocumentFromSearch(searchQuery, results);
|
|
1208
|
+
break;
|
|
1209
|
+
case '2':
|
|
1210
|
+
handleGenerateReport(searchQuery, results);
|
|
1211
|
+
break;
|
|
1212
|
+
case '3':
|
|
1213
|
+
handleGeneratePodcastFromSearch(searchQuery);
|
|
1214
|
+
break;
|
|
1215
|
+
case '4':
|
|
1216
|
+
handleSendSummary(searchQuery, results);
|
|
1217
|
+
break;
|
|
1218
|
+
case '5':
|
|
1219
|
+
handleSaveSearch(searchQuery, messageId);
|
|
1220
|
+
break;
|
|
1221
|
+
default:
|
|
1222
|
+
toast.info('Comando não implementado');
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
return (
|
|
1227
|
+
<>
|
|
1228
|
+
{/* Assistente + Editor Combined Container */}
|
|
1229
|
+
<div className={`${
|
|
1230
|
+
isFullPage
|
|
1231
|
+
? 'w-full h-full'
|
|
1232
|
+
: `fixed top-0 right-0 h-full z-40 transition-all duration-300 ease-in-out ${
|
|
1233
|
+
editingDocument && isExpanded ? '' : isExpanded ? 'w-[420px]' : 'w-20'
|
|
1234
|
+
} hidden md:block`
|
|
1235
|
+
}`}>
|
|
1236
|
+
<div className={`flex h-full overflow-hidden ${isFullPage ? 'max-w-full' : ''}`}>
|
|
1237
|
+
{/* Hidden file inputs */}
|
|
1238
|
+
<input
|
|
1239
|
+
ref={fileInputRef}
|
|
1240
|
+
type="file"
|
|
1241
|
+
accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,.csv"
|
|
1242
|
+
onChange={handleFileUpload}
|
|
1243
|
+
className="hidden"
|
|
1244
|
+
/>
|
|
1245
|
+
<input
|
|
1246
|
+
ref={audioInputRef}
|
|
1247
|
+
type="file"
|
|
1248
|
+
accept="audio/*"
|
|
1249
|
+
onChange={handleAudioUpload}
|
|
1250
|
+
className="hidden"
|
|
1251
|
+
/>
|
|
1252
|
+
|
|
1253
|
+
{/* Assistente Sidebar */}
|
|
1254
|
+
<div className={`h-full flex flex-col ${
|
|
1255
|
+
isFullPage
|
|
1256
|
+
? editingDocument ? 'w-1/2' : 'w-full'
|
|
1257
|
+
: `bg-card border-l border-border shadow-xl ${
|
|
1258
|
+
editingDocument && isExpanded ? 'w-[420px]' : 'w-full'
|
|
1259
|
+
}`
|
|
1260
|
+
}`}>
|
|
1261
|
+
|
|
1262
|
+
{/* Header - Toggle Button - Apenas visível quando não está em fullPage */}
|
|
1263
|
+
{!isFullPage && (
|
|
1264
|
+
<div className="border-b border-border flex items-center justify-between px-[14px] px-[18px] py-[16px]">
|
|
1265
|
+
{isExpanded && (
|
|
1266
|
+
<motion.div
|
|
1267
|
+
initial={{ opacity: 0, x: 20 }}
|
|
1268
|
+
animate={{ opacity: 1, x: 0 }}
|
|
1269
|
+
exit={{ opacity: 0, x: 20 }}
|
|
1270
|
+
className="flex items-center gap-2"
|
|
1271
|
+
>
|
|
1272
|
+
<XerticaOrbe size={32} />
|
|
1273
|
+
<span className="text-foreground">Assistente Xertica</span>
|
|
1274
|
+
</motion.div>
|
|
1275
|
+
)}
|
|
1276
|
+
|
|
1277
|
+
<div className="flex items-center gap-1">
|
|
1278
|
+
{isExpanded && (
|
|
1279
|
+
<Button
|
|
1280
|
+
variant="ghost"
|
|
1281
|
+
size="sm"
|
|
1282
|
+
onClick={() => navigate('/assistente')}
|
|
1283
|
+
className="p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
1284
|
+
title="Expandir assistente"
|
|
1285
|
+
>
|
|
1286
|
+
<Maximize2 className="w-4 h-4" />
|
|
1287
|
+
</Button>
|
|
1288
|
+
)}
|
|
1289
|
+
|
|
1290
|
+
<Button
|
|
1291
|
+
variant="ghost"
|
|
1292
|
+
size="sm"
|
|
1293
|
+
onClick={onToggle}
|
|
1294
|
+
className="p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
1295
|
+
>
|
|
1296
|
+
{isExpanded ? (
|
|
1297
|
+
<ChevronRight className="w-4 h-4" />
|
|
1298
|
+
) : (
|
|
1299
|
+
<PanelRight className="w-4 h-4" />
|
|
1300
|
+
)}
|
|
1301
|
+
</Button>
|
|
1302
|
+
</div>
|
|
1303
|
+
</div>
|
|
1304
|
+
)}
|
|
1305
|
+
|
|
1306
|
+
{/* Collapsed State - Icons Only */}
|
|
1307
|
+
{!isExpanded && !isFullPage && (
|
|
1308
|
+
<div className="flex flex-col items-center p-4 space-y-4">
|
|
1309
|
+
{/* Ícone do Assistente - Xertica Orbe */}
|
|
1310
|
+
<Tooltip>
|
|
1311
|
+
<TooltipTrigger asChild>
|
|
1312
|
+
<button
|
|
1313
|
+
onClick={onToggle}
|
|
1314
|
+
className="flex items-center justify-center hover:opacity-90 transition-opacity duration-200 cursor-pointer"
|
|
1315
|
+
>
|
|
1316
|
+
<XerticaOrbe size={32} />
|
|
1317
|
+
</button>
|
|
1318
|
+
</TooltipTrigger>
|
|
1319
|
+
<AssistantTooltipContent
|
|
1320
|
+
side="left"
|
|
1321
|
+
sideOffset={8}
|
|
1322
|
+
>
|
|
1323
|
+
<p>Assistente Xertica</p>
|
|
1324
|
+
</AssistantTooltipContent>
|
|
1325
|
+
</Tooltip>
|
|
1326
|
+
|
|
1327
|
+
<Tooltip>
|
|
1328
|
+
<TooltipTrigger asChild>
|
|
1329
|
+
<Button
|
|
1330
|
+
variant="ghost"
|
|
1331
|
+
size="sm"
|
|
1332
|
+
onClick={() => handleExpandWithTab('chat')}
|
|
1333
|
+
className="w-8 h-8 p-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
1334
|
+
>
|
|
1335
|
+
<MessageSquare className="w-4 h-4" />
|
|
1336
|
+
</Button>
|
|
1337
|
+
</TooltipTrigger>
|
|
1338
|
+
<AssistantTooltipContent
|
|
1339
|
+
side="left"
|
|
1340
|
+
sideOffset={8}
|
|
1341
|
+
>
|
|
1342
|
+
<p>Chat</p>
|
|
1343
|
+
</AssistantTooltipContent>
|
|
1344
|
+
</Tooltip>
|
|
1345
|
+
|
|
1346
|
+
<Tooltip>
|
|
1347
|
+
<TooltipTrigger asChild>
|
|
1348
|
+
<Button
|
|
1349
|
+
variant="ghost"
|
|
1350
|
+
size="sm"
|
|
1351
|
+
onClick={() => handleExpandWithTab('favoritos')}
|
|
1352
|
+
className="w-8 h-8 p-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
1353
|
+
>
|
|
1354
|
+
<Heart className="w-4 h-4" />
|
|
1355
|
+
</Button>
|
|
1356
|
+
</TooltipTrigger>
|
|
1357
|
+
<AssistantTooltipContent
|
|
1358
|
+
side="left"
|
|
1359
|
+
sideOffset={8}
|
|
1360
|
+
>
|
|
1361
|
+
<p>Favoritos</p>
|
|
1362
|
+
</AssistantTooltipContent>
|
|
1363
|
+
</Tooltip>
|
|
1364
|
+
|
|
1365
|
+
<Tooltip>
|
|
1366
|
+
<TooltipTrigger asChild>
|
|
1367
|
+
<Button
|
|
1368
|
+
variant="ghost"
|
|
1369
|
+
size="sm"
|
|
1370
|
+
onClick={() => handleExpandWithTab('historico')}
|
|
1371
|
+
className="w-8 h-8 p-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
1372
|
+
>
|
|
1373
|
+
<History className="w-4 h-4" />
|
|
1374
|
+
</Button>
|
|
1375
|
+
</TooltipTrigger>
|
|
1376
|
+
<AssistantTooltipContent
|
|
1377
|
+
side="left"
|
|
1378
|
+
sideOffset={8}
|
|
1379
|
+
>
|
|
1380
|
+
<p>Histórico</p>
|
|
1381
|
+
</AssistantTooltipContent>
|
|
1382
|
+
</Tooltip>
|
|
1383
|
+
</div>
|
|
1384
|
+
)}
|
|
1385
|
+
|
|
1386
|
+
{/* Expanded State - Full Content */}
|
|
1387
|
+
<AnimatePresence>
|
|
1388
|
+
{(isExpanded || isFullPage) && (
|
|
1389
|
+
<motion.div
|
|
1390
|
+
initial={isFullPage ? false : { opacity: 0, x: 50 }}
|
|
1391
|
+
animate={{ opacity: 1, x: 0 }}
|
|
1392
|
+
exit={isFullPage ? undefined : { opacity: 0, x: 50 }}
|
|
1393
|
+
transition={{ duration: 0.2 }}
|
|
1394
|
+
className="flex-1 flex flex-col overflow-hidden"
|
|
1395
|
+
>
|
|
1396
|
+
{/* Navigation Tabs - Oculto em modo fullPage */}
|
|
1397
|
+
{!isFullPage && (
|
|
1398
|
+
<div className="px-4 py-2 border-b border-border">
|
|
1399
|
+
<div className="flex gap-1">
|
|
1400
|
+
<Button
|
|
1401
|
+
variant={abaSelecionada === 'chat' ? 'default' : 'ghost'}
|
|
1402
|
+
size="sm"
|
|
1403
|
+
onClick={() => setAbaSelecionada('chat')}
|
|
1404
|
+
className="flex-1 h-8 text-xs"
|
|
1405
|
+
>
|
|
1406
|
+
<MessageSquare className="w-3 h-3 mr-1" />
|
|
1407
|
+
Chat
|
|
1408
|
+
</Button>
|
|
1409
|
+
<Button
|
|
1410
|
+
variant={abaSelecionada === 'historico' ? 'default' : 'ghost'}
|
|
1411
|
+
size="sm"
|
|
1412
|
+
onClick={() => setAbaSelecionada('historico')}
|
|
1413
|
+
className="flex-1 h-8 text-xs"
|
|
1414
|
+
>
|
|
1415
|
+
<History className="w-3 h-3 mr-1" />
|
|
1416
|
+
Histórico
|
|
1417
|
+
</Button>
|
|
1418
|
+
<Button
|
|
1419
|
+
variant={abaSelecionada === 'favoritos' ? 'default' : 'ghost'}
|
|
1420
|
+
size="sm"
|
|
1421
|
+
onClick={() => setAbaSelecionada('favoritos')}
|
|
1422
|
+
className="flex-1 h-8 text-xs"
|
|
1423
|
+
>
|
|
1424
|
+
<Heart className="w-3 h-3 mr-1" />
|
|
1425
|
+
Favoritos
|
|
1426
|
+
</Button>
|
|
1427
|
+
</div>
|
|
1428
|
+
</div>
|
|
1429
|
+
)}
|
|
1430
|
+
|
|
1431
|
+
{/* Content Area */}
|
|
1432
|
+
<div className="flex-1 overflow-hidden flex flex-col">
|
|
1433
|
+
{abaSelecionada === 'chat' && (
|
|
1434
|
+
<div className="flex-1 flex flex-col min-h-0 w-full">
|
|
1435
|
+
{/* API Key Warning Banner */}
|
|
1436
|
+
{!isApiKeyValid && (
|
|
1437
|
+
<div className="mx-4 mt-3 p-4 bg-[var(--toast-warning-bg)]/20 border-2 border-[var(--toast-warning-border)] rounded-lg">
|
|
1438
|
+
<div className="flex items-start gap-3">
|
|
1439
|
+
<AlertCircle className="w-5 h-5 text-[var(--toast-warning-icon)] flex-shrink-0 mt-0.5" />
|
|
1440
|
+
<div className="flex-1 space-y-3">
|
|
1441
|
+
<p className="text-small text-foreground">
|
|
1442
|
+
{!geminiApiKey ? (
|
|
1443
|
+
<strong>Nenhuma chave de API configurada</strong>
|
|
1444
|
+
) : (
|
|
1445
|
+
<strong>🔐 Chave de API inválida ou vazada</strong>
|
|
1446
|
+
)}
|
|
1447
|
+
</p>
|
|
1448
|
+
<p className="text-small text-muted-foreground">
|
|
1449
|
+
Configure uma nova chave do Google Gemini para ativar respostas inteligentes do assistente.
|
|
1450
|
+
</p>
|
|
1451
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
1452
|
+
<Button
|
|
1453
|
+
onClick={() => navigate('/settings')}
|
|
1454
|
+
size="sm"
|
|
1455
|
+
className="bg-[var(--toast-warning-icon)] hover:opacity-90 text-white rounded-[12px] shadow-sm"
|
|
1456
|
+
>
|
|
1457
|
+
Ir para Configurações
|
|
1458
|
+
</Button>
|
|
1459
|
+
<a
|
|
1460
|
+
href="https://aistudio.google.com/apikey"
|
|
1461
|
+
target="_blank"
|
|
1462
|
+
rel="noopener noreferrer"
|
|
1463
|
+
className="inline-flex items-center gap-1 text-small text-[var(--toast-warning-icon)] underline hover:opacity-80"
|
|
1464
|
+
>
|
|
1465
|
+
Obter chave gratuitamente
|
|
1466
|
+
<ExternalLink className="w-3 h-3" />
|
|
1467
|
+
</a>
|
|
1468
|
+
</div>
|
|
1469
|
+
</div>
|
|
1470
|
+
</div>
|
|
1471
|
+
</div>
|
|
1472
|
+
)}
|
|
1473
|
+
|
|
1474
|
+
{mensagens.length === 0 ? (
|
|
1475
|
+
<div className="flex-1 overflow-y-auto min-h-0">
|
|
1476
|
+
{/* Welcome Message */}
|
|
1477
|
+
<div className="p-6 text-center">
|
|
1478
|
+
<div className="mx-auto mb-4 flex items-center justify-center">
|
|
1479
|
+
<XerticaOrbe size={64} />
|
|
1480
|
+
</div>
|
|
1481
|
+
<h3 className="text-foreground mb-2">
|
|
1482
|
+
Olá, Ariel
|
|
1483
|
+
</h3>
|
|
1484
|
+
<p className="text-muted-foreground">
|
|
1485
|
+
Como posso ajudar?
|
|
1486
|
+
</p>
|
|
1487
|
+
</div>
|
|
1488
|
+
|
|
1489
|
+
{/* Suggestions */}
|
|
1490
|
+
<div className="px-4 pb-4 space-y-2">
|
|
1491
|
+
{sugestoes.map((sugestao) => (
|
|
1492
|
+
<button
|
|
1493
|
+
key={sugestao.id}
|
|
1494
|
+
onClick={() => setMensagem(sugestao.texto)}
|
|
1495
|
+
className="w-full p-3 text-left bg-muted hover:bg-accent rounded-lg transition-colors duration-200 text-foreground"
|
|
1496
|
+
>
|
|
1497
|
+
{sugestao.texto}
|
|
1498
|
+
</button>
|
|
1499
|
+
))}
|
|
1500
|
+
|
|
1501
|
+
<Button
|
|
1502
|
+
variant="ghost"
|
|
1503
|
+
size="sm"
|
|
1504
|
+
className="w-full justify-start text-muted-foreground hover:text-foreground"
|
|
1505
|
+
>
|
|
1506
|
+
<MoreHorizontal className="w-4 h-4 mr-2" />
|
|
1507
|
+
Mais sugestões
|
|
1508
|
+
</Button>
|
|
1509
|
+
</div>
|
|
1510
|
+
</div>
|
|
1511
|
+
) : (
|
|
1512
|
+
<ScrollArea className="flex-1 min-h-0 overflow-x-hidden">
|
|
1513
|
+
<div className="space-y-4 px-4 py-4 overflow-x-hidden w-full">
|
|
1514
|
+
{mensagens.map((msg) => (
|
|
1515
|
+
<motion.div
|
|
1516
|
+
key={msg.id}
|
|
1517
|
+
initial={{ opacity: 0, y: 10 }}
|
|
1518
|
+
animate={{ opacity: 1, y: 0 }}
|
|
1519
|
+
className={`flex gap-2 ${msg.type === 'user' ? 'justify-end' : 'justify-start'}`}
|
|
1520
|
+
>
|
|
1521
|
+
{/* Avatar do Assistente */}
|
|
1522
|
+
{msg.type === 'assistant' && (
|
|
1523
|
+
<div className="flex-shrink-0 pt-1">
|
|
1524
|
+
<XerticaOrbe size={32} />
|
|
1525
|
+
</div>
|
|
1526
|
+
)}
|
|
1527
|
+
|
|
1528
|
+
<div data-chat-message-container className={`max-w-[85%] min-w-0 w-auto ${msg.type === 'user' ? 'order-2' : 'order-1'}`}>
|
|
1529
|
+
<div className={cn(
|
|
1530
|
+
"rounded-[var(--radius-card)] px-4 py-2 break-words overflow-hidden overflow-x-hidden w-fit",
|
|
1531
|
+
msg.type === 'user'
|
|
1532
|
+
? "bg-primary text-primary-foreground shadow-sm"
|
|
1533
|
+
: "bg-muted text-foreground"
|
|
1534
|
+
)}>
|
|
1535
|
+
{/* Document Header with Edit and Download Buttons */}
|
|
1536
|
+
{msg.attachmentType === 'document' && (
|
|
1537
|
+
<div className="flex items-center justify-between mb-2 pb-2 border-b border-border min-w-0 overflow-hidden">
|
|
1538
|
+
<div className="flex items-center gap-2 min-w-0 flex-1 overflow-hidden">
|
|
1539
|
+
<FileText className="w-4 h-4 flex-shrink-0" />
|
|
1540
|
+
<span className="text-small break-words">{msg.attachmentName}</span>
|
|
1541
|
+
</div>
|
|
1542
|
+
<div className="flex items-center gap-1 flex-shrink-0">
|
|
1543
|
+
<Button
|
|
1544
|
+
variant="ghost"
|
|
1545
|
+
size="sm"
|
|
1546
|
+
onClick={() => msg.documentContent && msg.attachmentName && handleDownloadDocument(msg.documentContent, msg.attachmentName)}
|
|
1547
|
+
className="h-6 px-2 hover:bg-[var(--toast-success-bg)]/30 hover:text-[var(--toast-success-icon)]"
|
|
1548
|
+
title="Download"
|
|
1549
|
+
>
|
|
1550
|
+
<Download className="w-3 h-3" />
|
|
1551
|
+
</Button>
|
|
1552
|
+
<Button
|
|
1553
|
+
variant="ghost"
|
|
1554
|
+
size="sm"
|
|
1555
|
+
onClick={() => msg.documentContent && msg.documentTitle && handleEditDocument(msg.documentContent, msg.documentTitle)}
|
|
1556
|
+
className="h-6 px-2 hover:bg-[var(--toast-info-bg)]/30 hover:text-[var(--toast-info-icon)]"
|
|
1557
|
+
>
|
|
1558
|
+
<Edit className="w-3 h-3 mr-1" />
|
|
1559
|
+
Editar
|
|
1560
|
+
</Button>
|
|
1561
|
+
</div>
|
|
1562
|
+
</div>
|
|
1563
|
+
)}
|
|
1564
|
+
|
|
1565
|
+
{msg.attachmentType && msg.attachmentType !== 'podcast' && msg.attachmentType !== 'document' && (
|
|
1566
|
+
<div className={cn(
|
|
1567
|
+
"flex items-center gap-2 mb-2 pb-2 border-b min-w-0 overflow-hidden",
|
|
1568
|
+
msg.type === 'user' ? "border-primary-foreground/20" : "border-border"
|
|
1569
|
+
)}>
|
|
1570
|
+
{msg.attachmentType === 'file' && <FileText className="w-4 h-4 flex-shrink-0" />}
|
|
1571
|
+
{msg.attachmentType === 'audio' && <Music className="w-4 h-4 flex-shrink-0" />}
|
|
1572
|
+
{msg.attachmentType === 'image' && <ImageIcon className="w-4 h-4 flex-shrink-0" />}
|
|
1573
|
+
<span className={cn(
|
|
1574
|
+
"text-small break-words",
|
|
1575
|
+
msg.type === 'user' ? "text-primary-foreground/90" : "text-muted-foreground"
|
|
1576
|
+
)}>{msg.attachmentName}</span>
|
|
1577
|
+
</div>
|
|
1578
|
+
)}
|
|
1579
|
+
|
|
1580
|
+
{/* Conteúdo da mensagem */}
|
|
1581
|
+
{msg.type === 'user' ? (
|
|
1582
|
+
<p className="whitespace-pre-wrap break-words text-primary-foreground">{msg.content}</p>
|
|
1583
|
+
) : (
|
|
1584
|
+
<>
|
|
1585
|
+
{/* Alerta visual para mensagens de erro */}
|
|
1586
|
+
{(msg.content.includes('🔐') || msg.content.includes('❌')) && (
|
|
1587
|
+
<div className="mb-3 p-3 bg-[var(--toast-error-bg)]/20 border border-[var(--toast-error-border)] rounded-lg overflow-hidden">
|
|
1588
|
+
<div className="flex items-start gap-2 min-w-0">
|
|
1589
|
+
<AlertCircle className="w-5 h-5 text-[var(--toast-error-icon)] flex-shrink-0 mt-0.5" />
|
|
1590
|
+
<div className="flex-1 min-w-0 overflow-hidden">
|
|
1591
|
+
<MarkdownMessage
|
|
1592
|
+
content={msg.content}
|
|
1593
|
+
className="text-foreground"
|
|
1594
|
+
/>
|
|
1595
|
+
</div>
|
|
1596
|
+
</div>
|
|
1597
|
+
</div>
|
|
1598
|
+
)}
|
|
1599
|
+
{/* Mensagem normal */}
|
|
1600
|
+
{!(msg.content.includes('🔐') || msg.content.includes('❌')) && (
|
|
1601
|
+
<MarkdownMessage
|
|
1602
|
+
content={msg.content}
|
|
1603
|
+
className="text-foreground"
|
|
1604
|
+
/>
|
|
1605
|
+
)}
|
|
1606
|
+
</>
|
|
1607
|
+
)}
|
|
1608
|
+
|
|
1609
|
+
{/* Document Preview - Mostrar documento formatado */}
|
|
1610
|
+
{msg.attachmentType === 'document' && msg.documentContent && (
|
|
1611
|
+
<div className="mt-3 pt-3 border-t border-border overflow-hidden">
|
|
1612
|
+
<FormattedDocument
|
|
1613
|
+
content={msg.documentContent}
|
|
1614
|
+
maxPreviewLength={250}
|
|
1615
|
+
/>
|
|
1616
|
+
</div>
|
|
1617
|
+
)}
|
|
1618
|
+
|
|
1619
|
+
{/* Podcast Player */}
|
|
1620
|
+
{msg.attachmentType === 'podcast' && msg.audioUrl && (
|
|
1621
|
+
<div className="mt-3 pt-3 border-t border-border overflow-hidden">
|
|
1622
|
+
<div className="flex items-center justify-between mb-3 min-w-0 overflow-hidden">
|
|
1623
|
+
<div className="flex items-center gap-2 min-w-0 flex-1 overflow-hidden">
|
|
1624
|
+
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-[var(--chart-4)] via-[var(--chart-1)] to-[var(--chart-4)] flex items-center justify-center flex-shrink-0">
|
|
1625
|
+
<Radio className="w-3 h-3 text-white" />
|
|
1626
|
+
</div>
|
|
1627
|
+
<span className="text-small text-muted-foreground break-words">
|
|
1628
|
+
{msg.attachmentName}
|
|
1629
|
+
</span>
|
|
1630
|
+
</div>
|
|
1631
|
+
<Button
|
|
1632
|
+
variant="ghost"
|
|
1633
|
+
size="sm"
|
|
1634
|
+
onClick={() => msg.audioUrl && msg.attachmentName && handleDownloadPodcast(msg.audioUrl, msg.attachmentName)}
|
|
1635
|
+
className="h-6 px-2 hover:bg-[var(--toast-success-bg)]/30 hover:text-[var(--toast-success-icon)] flex-shrink-0"
|
|
1636
|
+
title="Download"
|
|
1637
|
+
>
|
|
1638
|
+
<Download className="w-3 h-3" />
|
|
1639
|
+
</Button>
|
|
1640
|
+
</div>
|
|
1641
|
+
<div className="bg-gradient-to-r from-[var(--chart-1)]/10 to-[var(--chart-4)]/10 rounded-lg p-2 overflow-hidden">
|
|
1642
|
+
<audio
|
|
1643
|
+
controls
|
|
1644
|
+
className="w-full max-w-full h-10 outline-none"
|
|
1645
|
+
>
|
|
1646
|
+
<source src={msg.audioUrl} type="audio/mpeg" />
|
|
1647
|
+
Seu navegador não suporta o elemento de áudio.
|
|
1648
|
+
</audio>
|
|
1649
|
+
</div>
|
|
1650
|
+
</div>
|
|
1651
|
+
)}
|
|
1652
|
+
|
|
1653
|
+
{/* Search Results */}
|
|
1654
|
+
{msg.attachmentType === 'search' && msg.searchResults && (
|
|
1655
|
+
<div className="mt-3 pt-3 border-t border-border space-y-4 overflow-hidden">
|
|
1656
|
+
{/* Search Results List */}
|
|
1657
|
+
<div className="overflow-hidden">
|
|
1658
|
+
<div className="flex items-center gap-2 mb-3">
|
|
1659
|
+
<Search className="w-4 h-4 text-[var(--chart-4)]" />
|
|
1660
|
+
<h4 className="text-foreground">
|
|
1661
|
+
Resultados Encontrados ({msg.searchResults.length})
|
|
1662
|
+
</h4>
|
|
1663
|
+
</div>
|
|
1664
|
+
<div className="space-y-2 overflow-hidden">
|
|
1665
|
+
{msg.searchResults.map((result) => (
|
|
1666
|
+
<div
|
|
1667
|
+
key={result.id}
|
|
1668
|
+
onClick={() => handleOpenSearchResult(result)}
|
|
1669
|
+
className="p-3 rounded-lg border border-border hover:bg-[var(--primary-light)] hover:border-[var(--primary-light-foreground)] transition-all cursor-pointer group overflow-hidden"
|
|
1670
|
+
>
|
|
1671
|
+
<div className="flex items-start justify-between gap-2 min-w-0">
|
|
1672
|
+
<div className="flex items-start gap-2 flex-1 min-w-0 overflow-hidden">
|
|
1673
|
+
<div className="mt-0.5 flex-shrink-0">
|
|
1674
|
+
{result.type === 'document' && <FileText className="w-4 h-4 text-[var(--chart-4)] group-hover:scale-110 transition-transform" />}
|
|
1675
|
+
{result.type === 'project' && <FolderOpen className="w-4 h-4 text-[var(--chart-1)] group-hover:scale-110 transition-transform" />}
|
|
1676
|
+
{result.type === 'conversation' && <MessageSquare className="w-4 h-4 text-[var(--chart-2)] group-hover:scale-110 transition-transform" />}
|
|
1677
|
+
{result.type === 'file' && <Folder className="w-4 h-4 text-[var(--chart-3)] group-hover:scale-110 transition-transform" />}
|
|
1678
|
+
{result.type === 'contact' && <Users className="w-4 h-4 text-[var(--chart-5)] group-hover:scale-110 transition-transform" />}
|
|
1679
|
+
</div>
|
|
1680
|
+
<div className="flex-1 min-w-0 overflow-hidden">
|
|
1681
|
+
<div className="flex items-start gap-2 min-w-0">
|
|
1682
|
+
<h5 className="text-small text-foreground break-words group-hover:text-primary transition-colors flex-1 min-w-0">
|
|
1683
|
+
{result.title}
|
|
1684
|
+
</h5>
|
|
1685
|
+
<span className="px-1.5 py-0.5 rounded text-small bg-[var(--chart-4)]/10 text-[var(--chart-4)] flex-shrink-0 self-start">
|
|
1686
|
+
{result.relevance}%
|
|
1687
|
+
</span>
|
|
1688
|
+
</div>
|
|
1689
|
+
<p className="text-sm text-muted-foreground mt-1 line-clamp-2 break-words">
|
|
1690
|
+
{result.description}
|
|
1691
|
+
</p>
|
|
1692
|
+
<div className="flex items-center gap-3 mt-2 flex-wrap min-w-0">
|
|
1693
|
+
<span className="text-sm text-muted-foreground flex items-center gap-1 min-w-0 max-w-full">
|
|
1694
|
+
<ExternalLink className="w-3 h-3 flex-shrink-0" />
|
|
1695
|
+
<span className="truncate">{result.path}</span>
|
|
1696
|
+
</span>
|
|
1697
|
+
{result.lastModified && (
|
|
1698
|
+
<span className="text-sm text-muted-foreground flex items-center gap-1 flex-shrink-0">
|
|
1699
|
+
<Clock className="w-3 h-3" />
|
|
1700
|
+
{result.lastModified}
|
|
1701
|
+
</span>
|
|
1702
|
+
)}
|
|
1703
|
+
</div>
|
|
1704
|
+
</div>
|
|
1705
|
+
</div>
|
|
1706
|
+
</div>
|
|
1707
|
+
</div>
|
|
1708
|
+
))}
|
|
1709
|
+
</div>
|
|
1710
|
+
</div>
|
|
1711
|
+
|
|
1712
|
+
{/* Search Sources */}
|
|
1713
|
+
{msg.searchSources && (
|
|
1714
|
+
<div>
|
|
1715
|
+
<h4 className="text-foreground mb-2">
|
|
1716
|
+
Fontes Consultadas
|
|
1717
|
+
</h4>
|
|
1718
|
+
<div className="flex flex-wrap gap-2 overflow-hidden">
|
|
1719
|
+
{msg.searchSources.map((source, index) => (
|
|
1720
|
+
<div
|
|
1721
|
+
key={index}
|
|
1722
|
+
className="px-3 py-1.5 rounded-full bg-muted border border-border max-w-full"
|
|
1723
|
+
>
|
|
1724
|
+
<span className="text-sm font-medium text-foreground break-words">{source.name}</span>
|
|
1725
|
+
<span className="text-sm text-muted-foreground ml-1 whitespace-nowrap">({source.count})</span>
|
|
1726
|
+
</div>
|
|
1727
|
+
))}
|
|
1728
|
+
</div>
|
|
1729
|
+
</div>
|
|
1730
|
+
)}
|
|
1731
|
+
|
|
1732
|
+
{/* Search Commands */}
|
|
1733
|
+
{msg.searchCommands && msg.searchResults && (
|
|
1734
|
+
<div>
|
|
1735
|
+
<h4 className="text-foreground mb-2">
|
|
1736
|
+
O que você pode fazer com estes resultados
|
|
1737
|
+
</h4>
|
|
1738
|
+
<div className="grid grid-cols-1 gap-2">
|
|
1739
|
+
{msg.searchCommands.map((command) => (
|
|
1740
|
+
<button
|
|
1741
|
+
key={command.id}
|
|
1742
|
+
onClick={() => handleExecuteSearchCommand(
|
|
1743
|
+
command.id,
|
|
1744
|
+
msg.content.replace('🔍 Pesquisa realizada com sucesso!', '').split('"')[1] || 'pesquisa',
|
|
1745
|
+
msg.searchResults!,
|
|
1746
|
+
msg.id
|
|
1747
|
+
)}
|
|
1748
|
+
disabled={executingCommand === command.id}
|
|
1749
|
+
className={`flex items-start gap-2 p-2 rounded-lg border border-border hover:bg-[var(--primary-light)] hover:border-[var(--primary-light-foreground)] transition-all text-left disabled:opacity-50 disabled:cursor-not-allowed ${
|
|
1750
|
+
savedSearches.includes(msg.id) && command.id === '5' ? 'bg-[var(--toast-warning-bg)]/20 border-[var(--toast-warning-border)]' : ''
|
|
1751
|
+
}`}
|
|
1752
|
+
>
|
|
1753
|
+
{executingCommand === command.id ? (
|
|
1754
|
+
<Loader2 className="w-5 h-5 animate-spin flex-shrink-0 text-primary" />
|
|
1755
|
+
) : (
|
|
1756
|
+
<span className="text-lg flex-shrink-0">{command.icon}</span>
|
|
1757
|
+
)}
|
|
1758
|
+
<div className="flex-1 min-w-0 overflow-hidden">
|
|
1759
|
+
<div className="text-sm font-medium text-foreground break-words">
|
|
1760
|
+
{command.id === '5' && savedSearches.includes(msg.id) ? '⭐ Pesquisa salva' : command.label}
|
|
1761
|
+
</div>
|
|
1762
|
+
<div className="text-sm text-muted-foreground break-words">
|
|
1763
|
+
{executingCommand === command.id ? 'Processando...' : command.description}
|
|
1764
|
+
</div>
|
|
1765
|
+
</div>
|
|
1766
|
+
</button>
|
|
1767
|
+
))}
|
|
1768
|
+
</div>
|
|
1769
|
+
</div>
|
|
1770
|
+
)}
|
|
1771
|
+
</div>
|
|
1772
|
+
)}
|
|
1773
|
+
</div>
|
|
1774
|
+
|
|
1775
|
+
{/* Message Actions */}
|
|
1776
|
+
<div className="flex items-center gap-2 mt-1 px-2">
|
|
1777
|
+
<span className="text-sm text-muted-foreground">
|
|
1778
|
+
{msg.timestamp.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}
|
|
1779
|
+
</span>
|
|
1780
|
+
|
|
1781
|
+
<Button
|
|
1782
|
+
variant="ghost"
|
|
1783
|
+
size="sm"
|
|
1784
|
+
onClick={() => handleToggleFavorite(msg.id)}
|
|
1785
|
+
className="h-6 w-6 p-0 text-muted-foreground hover:text-destructive"
|
|
1786
|
+
>
|
|
1787
|
+
<Heart className={`w-3 h-3 ${msg.isFavorite ? 'fill-current text-destructive' : ''}`} />
|
|
1788
|
+
</Button>
|
|
1789
|
+
|
|
1790
|
+
{/* Botão Gerar Podcast - apenas para mensagens do assistente */}
|
|
1791
|
+
{msg.type === 'assistant' && msg.attachmentType !== 'podcast' && (
|
|
1792
|
+
<Button
|
|
1793
|
+
variant="ghost"
|
|
1794
|
+
size="sm"
|
|
1795
|
+
onClick={() => handleGeneratePodcast(msg.id, msg.content)}
|
|
1796
|
+
disabled={generatingPodcastId === msg.id}
|
|
1797
|
+
className="h-6 w-6 p-0 text-muted-foreground hover:text-primary disabled:opacity-50"
|
|
1798
|
+
title="Gerar Podcast"
|
|
1799
|
+
>
|
|
1800
|
+
{generatingPodcastId === msg.id ? (
|
|
1801
|
+
<Loader2 className="w-3 h-3 animate-spin" />
|
|
1802
|
+
) : (
|
|
1803
|
+
<Radio className="w-3 h-3" />
|
|
1804
|
+
)}
|
|
1805
|
+
</Button>
|
|
1806
|
+
)}
|
|
1807
|
+
|
|
1808
|
+
<Button
|
|
1809
|
+
variant="ghost"
|
|
1810
|
+
size="sm"
|
|
1811
|
+
onClick={() => handleCopyMessage(msg.content, msg.id)}
|
|
1812
|
+
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
|
|
1813
|
+
>
|
|
1814
|
+
{copiedId === msg.id ? (
|
|
1815
|
+
<Check className="w-3 h-3 text-[var(--toast-success-icon)]" />
|
|
1816
|
+
) : (
|
|
1817
|
+
<Copy className="w-3 h-3" />
|
|
1818
|
+
)}
|
|
1819
|
+
</Button>
|
|
1820
|
+
</div>
|
|
1821
|
+
</div>
|
|
1822
|
+
</motion.div>
|
|
1823
|
+
))}
|
|
1824
|
+
|
|
1825
|
+
{/* Typing Indicator */}
|
|
1826
|
+
{isTyping && (
|
|
1827
|
+
<motion.div
|
|
1828
|
+
initial={{ opacity: 0, y: 10 }}
|
|
1829
|
+
animate={{ opacity: 1, y: 0 }}
|
|
1830
|
+
className="flex gap-2 justify-start"
|
|
1831
|
+
>
|
|
1832
|
+
{/* Avatar do Assistente */}
|
|
1833
|
+
<div className="flex-shrink-0 pt-1">
|
|
1834
|
+
<XerticaOrbe size={32} />
|
|
1835
|
+
</div>
|
|
1836
|
+
|
|
1837
|
+
<div className="bg-muted rounded-2xl px-4 py-3">
|
|
1838
|
+
<div className="flex gap-1">
|
|
1839
|
+
<motion.div
|
|
1840
|
+
className="w-2 h-2 bg-muted-foreground rounded-full"
|
|
1841
|
+
animate={{ y: [0, -8, 0] }}
|
|
1842
|
+
transition={{ repeat: Infinity, duration: 0.6, delay: 0 }}
|
|
1843
|
+
/>
|
|
1844
|
+
<motion.div
|
|
1845
|
+
className="w-2 h-2 bg-muted-foreground rounded-full"
|
|
1846
|
+
animate={{ y: [0, -8, 0] }}
|
|
1847
|
+
transition={{ repeat: Infinity, duration: 0.6, delay: 0.2 }}
|
|
1848
|
+
/>
|
|
1849
|
+
<motion.div
|
|
1850
|
+
className="w-2 h-2 bg-muted-foreground rounded-full"
|
|
1851
|
+
animate={{ y: [0, -8, 0] }}
|
|
1852
|
+
transition={{ repeat: Infinity, duration: 0.6, delay: 0.4 }}
|
|
1853
|
+
/>
|
|
1854
|
+
</div>
|
|
1855
|
+
</div>
|
|
1856
|
+
</motion.div>
|
|
1857
|
+
)}
|
|
1858
|
+
|
|
1859
|
+
<div ref={messagesEndRef} />
|
|
1860
|
+
</div>
|
|
1861
|
+
</ScrollArea>
|
|
1862
|
+
)}
|
|
1863
|
+
</div>
|
|
1864
|
+
)}
|
|
1865
|
+
|
|
1866
|
+
{(abaSelecionada === 'historico' || abaSelecionada === 'favoritos') && (
|
|
1867
|
+
<ScrollArea className="flex-1 min-h-0">
|
|
1868
|
+
<div className={`p-4 ${isFullPage ? 'mx-auto w-full max-w-6xl' : ''}`}>
|
|
1869
|
+
{/* New Conversation Button */}
|
|
1870
|
+
<Button
|
|
1871
|
+
variant="outline"
|
|
1872
|
+
size="sm"
|
|
1873
|
+
onClick={handleNovaConversa}
|
|
1874
|
+
className="w-full mb-4 justify-start"
|
|
1875
|
+
>
|
|
1876
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
1877
|
+
Nova Conversa
|
|
1878
|
+
</Button>
|
|
1879
|
+
|
|
1880
|
+
{/* Conversations List */}
|
|
1881
|
+
<div className="space-y-2">
|
|
1882
|
+
{conversasFiltradas.length === 0 ? (
|
|
1883
|
+
<div className="text-center py-8">
|
|
1884
|
+
<Heart className="w-12 h-12 mx-auto text-gray-300 dark:text-gray-600 mb-2" />
|
|
1885
|
+
<p className="text-muted-foreground">
|
|
1886
|
+
{abaSelecionada === 'favoritos'
|
|
1887
|
+
? 'Nenhuma conversa favorita ainda'
|
|
1888
|
+
: 'Nenhuma conversa no histórico'}
|
|
1889
|
+
</p>
|
|
1890
|
+
</div>
|
|
1891
|
+
) : (
|
|
1892
|
+
conversasFiltradas.map((conversa) => (
|
|
1893
|
+
<div
|
|
1894
|
+
key={conversa.id}
|
|
1895
|
+
onClick={() => handleSelecionarConversa(conversa.id)}
|
|
1896
|
+
className={`p-3 rounded-lg hover:bg-muted cursor-pointer transition-colors duration-200 border ${
|
|
1897
|
+
conversa.id === conversaAtual
|
|
1898
|
+
? 'border-primary bg-primary/5'
|
|
1899
|
+
: 'border-border'
|
|
1900
|
+
}`}
|
|
1901
|
+
>
|
|
1902
|
+
<div className="flex items-start justify-between mb-1">
|
|
1903
|
+
<h4 className="text-small text-gray-900 dark:text-gray-100 truncate flex-1">
|
|
1904
|
+
{conversa.titulo}
|
|
1905
|
+
</h4>
|
|
1906
|
+
<Button
|
|
1907
|
+
variant="ghost"
|
|
1908
|
+
size="sm"
|
|
1909
|
+
onClick={(e) => {
|
|
1910
|
+
e.stopPropagation();
|
|
1911
|
+
handleToggleFavoritaConversa(conversa.id);
|
|
1912
|
+
}}
|
|
1913
|
+
className="h-6 w-6 p-0 flex-shrink-0 ml-1"
|
|
1914
|
+
>
|
|
1915
|
+
<Heart className={`w-3 h-3 ${conversa.favorita ? 'text-red-500 fill-current' : 'text-gray-400'}`} />
|
|
1916
|
+
</Button>
|
|
1917
|
+
</div>
|
|
1918
|
+
{conversa.ultimaMensagem && (
|
|
1919
|
+
<p className="text-sm text-muted-foreground truncate mb-1">
|
|
1920
|
+
{conversa.ultimaMensagem}
|
|
1921
|
+
</p>
|
|
1922
|
+
)}
|
|
1923
|
+
<p className="text-sm text-muted-foreground">
|
|
1924
|
+
{conversa.timestamp}
|
|
1925
|
+
</p>
|
|
1926
|
+
</div>
|
|
1927
|
+
))
|
|
1928
|
+
)}
|
|
1929
|
+
</div>
|
|
1930
|
+
</div>
|
|
1931
|
+
</ScrollArea>
|
|
1932
|
+
)}
|
|
1933
|
+
</div>
|
|
1934
|
+
|
|
1935
|
+
{/* Modern Input Area - Only visible in chat mode */}
|
|
1936
|
+
{abaSelecionada === 'chat' && (
|
|
1937
|
+
<ModernChatInput
|
|
1938
|
+
value={mensagem}
|
|
1939
|
+
onChange={setMensagem}
|
|
1940
|
+
onSubmit={handleEnviarMensagem}
|
|
1941
|
+
onFileUpload={() => fileInputRef.current?.click()}
|
|
1942
|
+
onAudioUpload={() => audioInputRef.current?.click()}
|
|
1943
|
+
onVoiceRecording={(transcript) => {
|
|
1944
|
+
// Ao finalizar gravação, definir a mensagem transcrita e enviar
|
|
1945
|
+
setMensagem(transcript);
|
|
1946
|
+
toast.success('Áudio transcrito com sucesso!', {
|
|
1947
|
+
description: 'Sua mensagem foi convertida de voz para texto.',
|
|
1948
|
+
duration: 3000,
|
|
1949
|
+
});
|
|
1950
|
+
// Enviar automaticamente após um pequeno delay para o usuário ver a transcrição
|
|
1951
|
+
setTimeout(() => {
|
|
1952
|
+
if (!isTyping) {
|
|
1953
|
+
handleEnviarMensagem();
|
|
1954
|
+
}
|
|
1955
|
+
}, 1500);
|
|
1956
|
+
}}
|
|
1957
|
+
placeholder="Envie uma mensagem para Xertica"
|
|
1958
|
+
disabled={isTyping}
|
|
1959
|
+
isFullPage={isFullPage}
|
|
1960
|
+
/>
|
|
1961
|
+
)}
|
|
1962
|
+
</motion.div>
|
|
1963
|
+
)}
|
|
1964
|
+
</AnimatePresence>
|
|
1965
|
+
</div>
|
|
1966
|
+
|
|
1967
|
+
{/* Document Editor - Right Side */}
|
|
1968
|
+
{isFullPage ? (
|
|
1969
|
+
// Modo fullPage: editor fixo ocupando 50% da tela quando aberto
|
|
1970
|
+
editingDocument && (
|
|
1971
|
+
<div className="w-1/2 h-full bg-background border-l border-border">
|
|
1972
|
+
<DocumentEditor
|
|
1973
|
+
initialContent={editingDocument.content}
|
|
1974
|
+
initialTitle={editingDocument.title}
|
|
1975
|
+
onClose={handleCloseEditor}
|
|
1976
|
+
/>
|
|
1977
|
+
</div>
|
|
1978
|
+
)
|
|
1979
|
+
) : (
|
|
1980
|
+
// Modo sidebar: editor com animação deslizante
|
|
1981
|
+
<AnimatePresence>
|
|
1982
|
+
{editingDocument && isExpanded && (
|
|
1983
|
+
<motion.div
|
|
1984
|
+
initial={{ x: 600 }}
|
|
1985
|
+
animate={{ x: 0 }}
|
|
1986
|
+
exit={{ x: 600 }}
|
|
1987
|
+
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
|
1988
|
+
className="w-[600px] h-full bg-background border-l border-border"
|
|
1989
|
+
>
|
|
1990
|
+
<DocumentEditor
|
|
1991
|
+
initialContent={editingDocument.content}
|
|
1992
|
+
initialTitle={editingDocument.title}
|
|
1993
|
+
onClose={handleCloseEditor}
|
|
1994
|
+
/>
|
|
1995
|
+
</motion.div>
|
|
1996
|
+
)}
|
|
1997
|
+
</AnimatePresence>
|
|
1998
|
+
)}
|
|
1999
|
+
</div>
|
|
2000
|
+
</div>
|
|
2001
|
+
</>
|
|
2002
|
+
);
|
|
2003
|
+
}
|