xertica-ui 1.3.4 → 1.3.5

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.
@@ -1,6 +1,6 @@
1
- import React, { useState, useEffect, useRef } from 'react';
2
1
  import { useNavigate } from 'react-router';
3
2
  import { motion, AnimatePresence } from 'framer-motion';
3
+ import React, { useState, useEffect, useRef, Suspense } from 'react';
4
4
  import {
5
5
  MessageSquare,
6
6
  Heart,
@@ -33,8 +33,7 @@ import {
33
33
  Bookmark,
34
34
  Maximize2,
35
35
  AlertCircle,
36
- ThumbsUp,
37
- ThumbsDown,
36
+
38
37
  BookOpen,
39
38
  ArrowLeft,
40
39
  Table as TableIcon
@@ -46,34 +45,19 @@ import { DocumentEditor } from './DocumentEditor';
46
45
  import { MarkdownMessage } from './MarkdownMessage';
47
46
  import { FormattedDocument } from './FormattedDocument';
48
47
  import { ModernChatInput, ActionType } from './ModernChatInput';
49
- import {
50
- DropdownMenu,
51
- DropdownMenuContent,
52
- DropdownMenuItem,
53
- DropdownMenuTrigger,
54
- } from "./ui/dropdown-menu";
55
- import {
56
- Dialog,
57
- DialogContent,
58
- DialogDescription,
59
- DialogFooter,
60
- DialogHeader,
61
- DialogTitle,
62
- } from "./ui/dialog";
63
- import { Textarea } from "./ui/textarea";
48
+
49
+
64
50
  import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
65
51
  import { XerticaOrbe } from './XerticaOrbe';
66
- import { useApiKey } from '../contexts/ApiKeyContext';
67
52
  import { useAssistente } from '../contexts/AssistenteContext';
68
- import { callGeminiAPI, buildSystemPrompt, GeminiMessage } from '../utils/gemini';
69
53
  import { toast } from 'sonner';
70
54
  import { Tooltip, TooltipTrigger } from './ui/tooltip';
71
55
  import * as TooltipPrimitive from '@radix-ui/react-tooltip';
72
56
  import { cn } from './ui/utils';
73
57
  import { ASSISTANT_EXPANDED_WIDTH, ASSISTANT_COLLAPSED_WIDTH } from './layout-constants';
74
- // Import Chart Components
75
- import { Bar, BarChart, CartesianGrid, XAxis, Tooltip as RechartsTooltip, ResponsiveContainer } from "recharts";
76
- import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "./ui/chart";
58
+ // Import Chart Components - Lazy Loaded
59
+ const AssistantChart = React.lazy(() => import('./ui/AssistantChart'));
60
+ import { ChartConfig } from "./ui/chart";
77
61
  import {
78
62
  Table,
79
63
  TableBody,
@@ -155,7 +139,8 @@ export function AssistenteXertica({
155
139
  customResponses = []
156
140
  }: AssistenteXerticaProps) {
157
141
  const navigate = useNavigate();
158
- const { geminiApiKey, isApiKeyValid } = useApiKey();
142
+
143
+
159
144
  const {
160
145
  conversaAtual,
161
146
  setConversaAtual,
@@ -179,10 +164,7 @@ export function AssistenteXertica({
179
164
  const [copiedId, setCopiedId] = useState<string | null>(null);
180
165
  const [generatingPodcastId, setGeneratingPodcastId] = useState<string | null>(null);
181
166
  const [executingCommand, setExecutingCommand] = useState<string | null>(null);
182
- const [feedbackDialogOpen, setFeedbackDialogOpen] = useState(false);
183
- const [feedbackMessageId, setFeedbackMessageId] = useState<string | null>(null);
184
- const [feedbackCategory, setFeedbackCategory] = useState<string | null>(null);
185
- const [feedbackReason, setFeedbackReason] = useState("");
167
+
186
168
  const messagesEndRef = useRef<HTMLDivElement>(null);
187
169
  const fileInputRef = useRef<HTMLInputElement>(null);
188
170
  const audioInputRef = useRef<HTMLInputElement>(null);
@@ -286,52 +268,7 @@ export function AssistenteXertica({
286
268
  }, 1500);
287
269
  };
288
270
 
289
- const handleEvaluation = (messageId: string, evaluation: 'like' | 'dislike', reason?: string) => {
290
- setConversas(prev => prev.map(conv => {
291
- if (conv.id === conversaAtual) {
292
- return {
293
- ...conv,
294
- mensagens: conv.mensagens.map(msg =>
295
- msg.id === messageId ? { ...msg, evaluation, evaluationReason: reason } : msg
296
- )
297
- };
298
- }
299
- return conv;
300
- }));
301
-
302
- if (evaluation === 'like') {
303
- toast.success("Obrigado por avaliar positivamente!");
304
- } else if (evaluation === 'dislike' && reason) {
305
- toast.success("Obrigado pelo seu feedback. Vamos melhorar.");
306
- }
307
- };
308
-
309
- const openFeedbackDialog = (messageId: string, category: string | null = null) => {
310
- setFeedbackMessageId(messageId);
311
- setFeedbackCategory(category);
312
- setFeedbackReason("");
313
- setFeedbackDialogOpen(true);
314
- };
315
-
316
- const submitFeedbackDialog = () => {
317
- if (!feedbackMessageId) return;
318
271
 
319
- let finalReason = "";
320
- if (feedbackCategory) {
321
- // Se tem categoria pré-definida, o comentário é opcional
322
- finalReason = feedbackReason.trim() ? `${feedbackCategory}: ${feedbackReason}` : feedbackCategory;
323
- } else {
324
- // Se é "Outros", o motivo é obrigatório
325
- if (!feedbackReason.trim()) return;
326
- finalReason = feedbackReason;
327
- }
328
-
329
- handleEvaluation(feedbackMessageId, 'dislike', finalReason);
330
- setFeedbackDialogOpen(false);
331
- setFeedbackMessageId(null);
332
- setFeedbackReason("");
333
- setFeedbackCategory(null);
334
- };
335
272
 
336
273
  const conversaAtiva = conversas.find(c => c.id === conversaAtual);
337
274
  const mensagens = conversaAtiva?.mensagens || [];
@@ -345,100 +282,26 @@ export function AssistenteXertica({
345
282
  scrollToBottom();
346
283
  }, [mensagens, isTyping]);
347
284
 
348
- // Resposta da IA usando Gemini API
285
+ // Resposta simulada da IA
349
286
  const simularRespostaIA = async (mensagemUsuario: string) => {
350
287
  setIsTyping(true);
351
288
 
352
289
  try {
353
- let novaMensagemParcial: Partial<Message> = {};
354
290
 
355
- // Se a chave da API for válida e não estiver em modo demo, usar Gemini
356
- if (isApiKeyValid && geminiApiKey && !demoMode) {
357
- // Construir histórico da conversa para o Gemini
358
- const conversaAtiva = conversas.find(c => c.id === conversaAtual);
359
- const historico: GeminiMessage[] = [];
360
-
361
- // Adicionar prompt do sistema
362
- const systemPrompt = buildSystemPrompt();
363
-
364
- // Converter mensagens anteriores para formato Gemini (últimas 10 mensagens)
365
- const mensagensRecentes = (conversaAtiva?.mensagens || []).slice(-10);
366
- for (const msg of mensagensRecentes) {
367
- if (msg.type === 'user') {
368
- // Remover prefixos de ação antes de enviar ao Gemini
369
- let conteudoLimpo = msg.content
370
- .replace(/^📄 \[Criar documento\] /, '')
371
- .replace(/^🎙️ \[Gerar podcast\] /, '')
372
- .replace(/^🔍 \[Pesquisar\] /, '');
373
-
374
- historico.push({
375
- role: 'user',
376
- parts: [{ text: conteudoLimpo }]
377
- });
378
- } else if (msg.type === 'assistant') {
379
- historico.push({
380
- role: 'model',
381
- parts: [{ text: msg.content }]
382
- });
383
- }
384
- }
385
291
 
386
- // Se não houver histórico, adicionar o prompt do sistema como primeira mensagem
387
- if (historico.length === 0) {
388
- historico.push({
389
- role: 'user',
390
- parts: [{ text: systemPrompt }]
391
- });
392
- historico.push({
393
- role: 'model',
394
- parts: [{ text: 'Entendido! Estou pronto para ajudar você com a plataforma Xertica.ai. Como posso ajudá-lo hoje?' }]
395
- });
396
- }
292
+ let novaMensagemParcial: Partial<Message> = {};
397
293
 
398
- try {
399
- const respostaTexto = await callGeminiAPI(geminiApiKey, mensagemUsuario, historico);
400
- novaMensagemParcial = { content: respostaTexto };
401
- } catch (error) {
402
- console.error('Erro ao chamar API Gemini:', error);
403
- // Usar a mensagem de erro do utilitário se disponível
404
- const errorMessage = error instanceof Error ? error.message : '❌ Erro desconhecido';
405
-
406
- // Verificar se é erro de chave vazada ou 403
407
- const isLeakedKeyError = errorMessage.toLowerCase().includes('leaked') ||
408
- errorMessage.toLowerCase().includes('reported') ||
409
- errorMessage.toLowerCase().includes('403');
410
-
411
- if (isLeakedKeyError) {
412
- toast.error('Chave de API desativada por segurança', {
413
- description: 'Sua chave foi reportada como vazada. Gere uma nova em Google AI Studio.',
414
- duration: 6000,
415
- });
416
- novaMensagemParcial = {
417
- content: `🔐 **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.`
418
- };
419
- } else {
420
- toast.error('Erro ao processar mensagem', {
421
- description: 'Verifique sua chave de API nas Configurações.',
422
- duration: 4000,
423
- });
424
- novaMensagemParcial = {
425
- content: `**Erro ao processar sua mensagem**\n\n${errorMessage}\n\n💡 **Dica:** Verifique se sua chave de API está configurada corretamente em **Configurações > API**.`
426
- };
427
- }
428
- }
429
- } else {
430
- // Fallback para respostas simuladas se não houver API key ou em modo demo
431
- await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1000));
294
+ // Fallback para respostas simuladas se não houver API key ou em modo demo
295
+ await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1000));
432
296
 
433
- // Passar customResponses para gerarResposta
434
- const resultadoMock = gerarResposta(mensagemUsuario, customResponses);
297
+ // Passar customResponses para gerarResposta
298
+ const resultadoMock = gerarResposta(mensagemUsuario, customResponses);
435
299
 
436
- if (typeof resultadoMock === 'string') {
437
- novaMensagemParcial = { content: resultadoMock };
438
- } else {
439
- // Se for objeto (MockResponse), usar diretamente
440
- novaMensagemParcial = resultadoMock;
441
- }
300
+ if (typeof resultadoMock === 'string') {
301
+ novaMensagemParcial = { content: resultadoMock };
302
+ } else {
303
+ // Se for objeto (MockResponse), usar diretamente
304
+ novaMensagemParcial = resultadoMock;
442
305
  }
443
306
 
444
307
  const novaMensagem: Message = {
@@ -1587,43 +1450,7 @@ Este documento fornece uma base sólida para entender e trabalhar com ${temaLimp
1587
1450
  {abaSelecionada === 'chat' && (
1588
1451
  <div className="flex-1 flex flex-col min-h-0 w-full">
1589
1452
  {/* API Key Warning Banner */}
1590
- {!isApiKeyValid && (
1591
- <div className="mx-4 mt-3 p-4 bg-[var(--toast-warning-bg)]/20 border-2 border-[var(--toast-warning-border)] rounded-lg">
1592
- <div className="flex items-start gap-3">
1593
- <AlertCircle className="w-5 h-5 text-[var(--toast-warning-icon)] flex-shrink-0 mt-0.5" />
1594
- <div className="flex-1 space-y-3">
1595
- <p className="text-small text-foreground">
1596
- {!geminiApiKey ? (
1597
- <strong>Nenhuma chave de API configurada</strong>
1598
- ) : (
1599
- <strong>🔐 Chave de API inválida ou vazada</strong>
1600
- )}
1601
- </p>
1602
- <p className="text-small text-muted-foreground">
1603
- Configure uma nova chave do Google Gemini para ativar respostas inteligentes do assistente.
1604
- </p>
1605
- <div className="flex flex-wrap items-center gap-2">
1606
- <Button
1607
- onClick={() => navigate('/settings')}
1608
- size="sm"
1609
- className="bg-[var(--toast-warning-icon)] hover:opacity-90 text-white rounded-[12px] shadow-sm"
1610
- >
1611
- Ir para Configurações
1612
- </Button>
1613
- <a
1614
- href="https://aistudio.google.com/apikey"
1615
- target="_blank"
1616
- rel="noopener noreferrer"
1617
- className="inline-flex items-center gap-1 text-small text-[var(--toast-warning-icon)] underline hover:opacity-80"
1618
- >
1619
- Obter chave gratuitamente
1620
- <ExternalLink className="w-3 h-3" />
1621
- </a>
1622
- </div>
1623
- </div>
1624
- </div>
1625
- </div>
1626
- )}
1453
+
1627
1454
 
1628
1455
  {mensagens.length === 0 ? (
1629
1456
  <div className="flex-1 overflow-y-auto min-h-0">
@@ -1805,30 +1632,15 @@ Este documento fornece uma base sólida para entender e trabalhar com ${temaLimp
1805
1632
  {/* Chart Rendering */}
1806
1633
  {msg.chartData && msg.chartConfig && (
1807
1634
  <div className="mt-4 mb-2 p-2 bg-background/50 rounded-lg border border-border">
1808
- <ChartContainer config={msg.chartConfig}>
1809
- <BarChart accessibilityLayer data={msg.chartData}>
1810
- <CartesianGrid vertical={false} />
1811
- <XAxis
1812
- dataKey="month"
1813
- tickLine={false}
1814
- tickMargin={10}
1815
- axisLine={false}
1816
- tickFormatter={(value) => value.slice(0, 3)}
1817
- />
1818
- <ChartTooltip
1819
- cursor={false}
1820
- content={<ChartTooltipContent indicator="dashed" />}
1821
- />
1822
- <Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
1823
- <Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
1824
- </BarChart>
1825
- </ChartContainer>
1635
+ <Suspense fallback={<div className="h-[200px] w-full flex items-center justify-center bg-muted/20 rounded-lg"><Loader2 className="w-8 h-8 animate-spin text-muted-foreground" /></div>}>
1636
+ <AssistantChart data={msg.chartData} config={msg.chartConfig} />
1637
+ </Suspense>
1826
1638
  </div>
1827
1639
  )}
1828
1640
 
1829
1641
  {/* Table Rendering */}
1830
1642
  {msg.tableData && (
1831
- <div className="mt-4 mb-2 rounded-md border">
1643
+ <div className="mt-4 mb-2 rounded-md border w-full max-w-full overflow-hidden">
1832
1644
  <Table>
1833
1645
  {msg.tableData.caption && <TableCaption>{msg.tableData.caption}</TableCaption>}
1834
1646
  <TableHeader>
@@ -2065,56 +1877,7 @@ Este documento fornece uma base sólida para entender e trabalhar com ${temaLimp
2065
1877
  </Button>
2066
1878
  </div>
2067
1879
 
2068
- {/* Feedback UI */}
2069
- {msg.type === 'assistant' && (
2070
- <div className="px-2 pb-2">
2071
- <div className="flex items-center gap-1 mt-2">
2072
- <span className="text-xs text-muted-foreground mr-2">Essa resposta foi útil?</span>
2073
- <Button
2074
- variant="ghost"
2075
- size="icon"
2076
- className={cn(
2077
- "h-6 w-6 rounded-full hover:bg-green-100 dark:hover:bg-green-900/20 hover:text-green-600",
2078
- msg.evaluation === 'like' && "text-green-600 bg-green-100 dark:bg-green-900/20"
2079
- )}
2080
- onClick={() => handleEvaluation(msg.id, 'like')}
2081
- title="Gostei"
2082
- >
2083
- <ThumbsUp className="h-3.5 w-3.5" />
2084
- </Button>
2085
-
2086
- <DropdownMenu>
2087
- <DropdownMenuTrigger asChild>
2088
- <Button
2089
- variant="ghost"
2090
- size="icon"
2091
- className={cn(
2092
- "h-6 w-6 rounded-full hover:bg-red-100 dark:hover:bg-red-900/20 hover:text-red-600",
2093
- msg.evaluation === 'dislike' && "text-red-600 bg-red-100 dark:bg-red-900/20"
2094
- )}
2095
- title="Não gostei"
2096
- >
2097
- <ThumbsDown className="h-3.5 w-3.5" />
2098
- </Button>
2099
- </DropdownMenuTrigger>
2100
- <DropdownMenuContent align="start">
2101
- <DropdownMenuItem onClick={() => openFeedbackDialog(msg.id, 'Não era o que eu procurava')}>
2102
- Não era o que eu procurava
2103
- </DropdownMenuItem>
2104
- <DropdownMenuItem onClick={() => openFeedbackDialog(msg.id, 'Informação incorreta')}>
2105
- Informação incorreta
2106
- </DropdownMenuItem>
2107
- <DropdownMenuItem onClick={() => openFeedbackDialog(msg.id, 'Resposta incompleta')}>
2108
- Resposta incompleta
2109
- </DropdownMenuItem>
2110
- <DropdownMenuItem onClick={() => openFeedbackDialog(msg.id, null)}>
2111
- Outros...
2112
- </DropdownMenuItem>
2113
- </DropdownMenuContent>
2114
- </DropdownMenu>
2115
- </div>
2116
- </div>
2117
- )}
1880
+
2118
1881
  </div>
2119
1882
  </motion.div>
2120
1883
  ))}
@@ -2160,37 +1923,7 @@ Este documento fornece uma base sólida para entender e trabalhar com ${temaLimp
2160
1923
  </div>
2161
1924
  )}
2162
1925
 
2163
- <Dialog open={feedbackDialogOpen} onOpenChange={setFeedbackDialogOpen}>
2164
- <DialogContent className="sm:max-w-[600px]">
2165
- <DialogHeader>
2166
- <DialogTitle className="pr-8">
2167
- {feedbackCategory ? `Enviar feedback: ${feedbackCategory}` : 'Enviar feedback'}
2168
- </DialogTitle>
2169
- <DialogDescription>
2170
- {feedbackCategory
2171
- ? "Gostaria de adicionar algum comentário? (Opcional)"
2172
- : "Conte-nos por que essa resposta não foi útil para que possamos melhorar."}
2173
- </DialogDescription>
2174
- </DialogHeader>
2175
- <div className="grid gap-4 py-4 px-6">
2176
- <Textarea
2177
- className="min-h-[100px]"
2178
- placeholder={feedbackCategory ? "Comentário adicional..." : "Descreva o motivo..."}
2179
- value={feedbackReason}
2180
- onChange={(e) => setFeedbackReason(e.target.value)}
2181
- />
2182
- </div>
2183
- <DialogFooter>
2184
- <Button variant="outline" onClick={() => setFeedbackDialogOpen(false)}>Cancelar</Button>
2185
- <Button
2186
- onClick={submitFeedbackDialog}
2187
- disabled={!feedbackCategory && !feedbackReason.trim()}
2188
- >
2189
- {feedbackCategory ? 'Confirmar e Enviar' : 'Enviar feedback'}
2190
- </Button>
2191
- </DialogFooter>
2192
- </DialogContent>
2193
- </Dialog>
1926
+
2194
1927
 
2195
1928
  {(abaSelecionada === 'historico' || abaSelecionada === 'favoritos') && (
2196
1929
  <ScrollArea className="flex-1 min-h-0">
@@ -13,9 +13,7 @@ import { Badge } from './ui/badge';
13
13
  import { useLayout } from '../contexts/LayoutContext';
14
14
  import {
15
15
  SIDEBAR_EXPANDED_WIDTH,
16
- SIDEBAR_COLLAPSED_WIDTH,
17
- ASSISTANT_EXPANDED_WIDTH,
18
- ASSISTANT_COLLAPSED_WIDTH
16
+ SIDEBAR_COLLAPSED_WIDTH
19
17
  } from './layout-constants';
20
18
 
21
19
  interface HomeContentProps {
@@ -44,8 +42,7 @@ export function HomeContent({ }: HomeContentProps) {
44
42
  return (
45
43
  <div
46
44
  style={{
47
- paddingLeft: sidebarExpanded ? SIDEBAR_EXPANDED_WIDTH : SIDEBAR_COLLAPSED_WIDTH,
48
- paddingRight: assistenteExpanded ? ASSISTANT_EXPANDED_WIDTH : ASSISTANT_COLLAPSED_WIDTH
45
+ paddingLeft: sidebarExpanded ? SIDEBAR_EXPANDED_WIDTH : SIDEBAR_COLLAPSED_WIDTH
49
46
  }}
50
47
  className={`flex-1 flex flex-col overflow-hidden transition-all duration-300`}
51
48
  >