xertica-ui 2.1.2 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +1 -1
  3. package/bin/cli.ts +1 -1
  4. package/bin/generate-tokens.ts +13 -7
  5. package/components/assistant/xertica-assistant/index.ts +2 -0
  6. package/components/assistant/xertica-assistant/parts/AssistantCollapsedView.tsx +97 -0
  7. package/components/assistant/xertica-assistant/parts/AssistantConversationList.tsx +104 -0
  8. package/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.tsx +81 -0
  9. package/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.tsx +86 -0
  10. package/components/assistant/xertica-assistant/parts/AssistantHeader.tsx +77 -0
  11. package/components/assistant/xertica-assistant/parts/AssistantMessageBubble.tsx +573 -0
  12. package/components/assistant/xertica-assistant/parts/AssistantTabBar.tsx +65 -0
  13. package/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.tsx +41 -0
  14. package/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.tsx +98 -0
  15. package/components/assistant/xertica-assistant/parts/index.ts +16 -0
  16. package/components/assistant/xertica-assistant/types.ts +139 -0
  17. package/components/assistant/xertica-assistant/use-assistant.ts +559 -0
  18. package/components/assistant/xertica-assistant/xertica-assistant.stories.tsx +200 -0
  19. package/components/assistant/xertica-assistant/xertica-assistant.tsx +198 -1460
  20. package/components/brand/theme-toggle/ThemeToggle.tsx +8 -27
  21. package/components/hooks/index.ts +3 -0
  22. package/components/hooks/use-layout-shortcuts.ts +46 -0
  23. package/components/layout/sidebar/index.ts +2 -0
  24. package/components/layout/sidebar/sidebar.stories.tsx +160 -8
  25. package/components/layout/sidebar/sidebar.tsx +606 -497
  26. package/components/layout/sidebar/use-sidebar.ts +104 -0
  27. package/components/media/audio-player/AudioPlayer.tsx +131 -206
  28. package/components/media/audio-player/use-audio-player.ts +298 -0
  29. package/components/pages/home-page/HomePage.tsx +1 -1
  30. package/components/pages/template-content/TemplateContent.tsx +5 -5
  31. package/components/pages/template-page/TemplatePage.tsx +5 -5
  32. package/components/shared/CustomTooltipContent.tsx +52 -0
  33. package/components/shared/layout-constants.ts +1 -1
  34. package/components/ui/chart/chart.stories.tsx +966 -7
  35. package/components/ui/chart/chart.tsx +918 -45
  36. package/components/ui/file-upload/file-upload.stories.tsx +100 -0
  37. package/components/ui/file-upload/file-upload.tsx +14 -74
  38. package/components/ui/file-upload/index.ts +1 -0
  39. package/components/ui/file-upload/use-file-upload.ts +181 -0
  40. package/components/ui/pagination/index.ts +2 -0
  41. package/components/ui/pagination/pagination.stories.tsx +94 -0
  42. package/components/ui/pagination/use-pagination.ts +194 -0
  43. package/components/ui/rich-text-editor/index.ts +2 -0
  44. package/components/ui/rich-text-editor/rich-text-editor.stories.tsx +129 -1
  45. package/components/ui/rich-text-editor/rich-text-editor.tsx +86 -305
  46. package/components/ui/rich-text-editor/use-rich-text-editor.ts +439 -0
  47. package/components/ui/stepper/index.ts +3 -1
  48. package/components/ui/stepper/stepper.stories.tsx +116 -0
  49. package/components/ui/stepper/stepper.tsx +4 -4
  50. package/components/ui/stepper/use-stepper.ts +137 -0
  51. package/components/ui/tree-view/index.ts +4 -1
  52. package/components/ui/tree-view/tree-view.stories.tsx +110 -4
  53. package/components/ui/tree-view/tree-view.tsx +17 -125
  54. package/components/ui/tree-view/use-tree-view.ts +229 -0
  55. package/contexts/AssistenteContext.tsx +17 -54
  56. package/contexts/BrandColorsContext.tsx +6 -17
  57. package/contexts/LayoutContext.tsx +5 -31
  58. package/dist/AssistantChart-BAudAfne.cjs +3591 -0
  59. package/dist/AssistantChart-BP8upjMk.js +3565 -0
  60. package/dist/AudioPlayer-1ypwE2Wh.cjs +936 -0
  61. package/dist/AudioPlayer-DuKXrCfy.js +937 -0
  62. package/dist/CustomTooltipContent-DHjkY0ww.js +40 -0
  63. package/dist/CustomTooltipContent-c_K-DWRr.cjs +56 -0
  64. package/dist/LanguageContext-BwhwC3G2.js +657 -0
  65. package/dist/LanguageContext-DvUt5jBg.cjs +656 -0
  66. package/dist/LayoutContext-BDmcZfMH.cjs +84 -0
  67. package/dist/LayoutContext-dbQvdC4O.js +85 -0
  68. package/dist/ThemeContext-RTy1m2Uq.js +82 -0
  69. package/dist/ThemeContext-bSzuOit2.cjs +81 -0
  70. package/dist/VerifyEmailPage-C_ihbcth.js +2828 -0
  71. package/dist/VerifyEmailPage-Dt7zgA4w.cjs +2827 -0
  72. package/dist/XerticaProvider-CW9hpCdF.cjs +39 -0
  73. package/dist/XerticaProvider-siSt9uG2.js +40 -0
  74. package/dist/XerticaXLogo-D8jf0SNv.cjs +214 -0
  75. package/dist/XerticaXLogo-fAJMy3H4.js +215 -0
  76. package/dist/assistant.cjs.js +2 -1
  77. package/dist/assistant.es.js +3 -2
  78. package/dist/brand.cjs.js +2 -2
  79. package/dist/brand.es.js +2 -2
  80. package/dist/cli.js +14 -8
  81. package/dist/components/assistant/xertica-assistant/index.d.ts +2 -0
  82. package/dist/components/assistant/xertica-assistant/parts/AssistantCollapsedView.d.ts +13 -0
  83. package/dist/components/assistant/xertica-assistant/parts/AssistantConversationList.d.ts +16 -0
  84. package/dist/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.d.ts +17 -0
  85. package/dist/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.d.ts +19 -0
  86. package/dist/components/assistant/xertica-assistant/parts/AssistantHeader.d.ts +11 -0
  87. package/dist/components/assistant/xertica-assistant/parts/AssistantMessageBubble.d.ts +29 -0
  88. package/dist/components/assistant/xertica-assistant/parts/AssistantTabBar.d.ts +13 -0
  89. package/dist/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.d.ts +4 -0
  90. package/dist/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.d.ts +17 -0
  91. package/dist/components/assistant/xertica-assistant/parts/index.d.ts +16 -0
  92. package/dist/components/assistant/xertica-assistant/types.d.ts +106 -0
  93. package/dist/components/assistant/xertica-assistant/use-assistant.d.ts +125 -0
  94. package/dist/components/assistant/xertica-assistant/xertica-assistant.d.ts +8 -97
  95. package/dist/components/hooks/index.d.ts +3 -0
  96. package/dist/components/hooks/use-layout-shortcuts.d.ts +22 -0
  97. package/dist/components/layout/sidebar/index.d.ts +2 -0
  98. package/dist/components/layout/sidebar/sidebar.d.ts +80 -0
  99. package/dist/components/layout/sidebar/use-sidebar.d.ts +22 -0
  100. package/dist/components/media/audio-player/AudioPlayer.d.ts +4 -1
  101. package/dist/components/media/audio-player/use-audio-player.d.ts +72 -0
  102. package/dist/components/shared/CustomTooltipContent.d.ts +20 -0
  103. package/dist/components/shared/layout-constants.d.ts +1 -1
  104. package/dist/components/ui/alert/alert.d.ts +1 -1
  105. package/dist/components/ui/badge/badge.d.ts +1 -1
  106. package/dist/components/ui/button/button.d.ts +2 -2
  107. package/dist/components/ui/chart/chart.d.ts +162 -5
  108. package/dist/components/ui/file-upload/file-upload.d.ts +2 -0
  109. package/dist/components/ui/file-upload/index.d.ts +1 -0
  110. package/dist/components/ui/file-upload/use-file-upload.d.ts +49 -0
  111. package/dist/components/ui/pagination/index.d.ts +2 -0
  112. package/dist/components/ui/pagination/use-pagination.d.ts +78 -0
  113. package/dist/components/ui/rich-text-editor/index.d.ts +2 -0
  114. package/dist/components/ui/rich-text-editor/use-rich-text-editor.d.ts +107 -0
  115. package/dist/components/ui/stepper/index.d.ts +3 -1
  116. package/dist/components/ui/stepper/stepper.d.ts +2 -2
  117. package/dist/components/ui/stepper/use-stepper.d.ts +60 -0
  118. package/dist/components/ui/tree-view/index.d.ts +4 -1
  119. package/dist/components/ui/tree-view/tree-view.d.ts +4 -6
  120. package/dist/components/ui/tree-view/use-tree-view.d.ts +60 -0
  121. package/dist/contexts/AssistenteContext.d.ts +10 -49
  122. package/dist/hooks.cjs.js +30 -10
  123. package/dist/hooks.es.js +25 -4
  124. package/dist/index.cjs.js +20 -9
  125. package/dist/index.es.js +38 -27
  126. package/dist/layout.cjs.js +82 -1
  127. package/dist/layout.es.js +83 -2
  128. package/dist/media.cjs.js +1 -1
  129. package/dist/media.es.js +1 -1
  130. package/dist/pages.cjs.js +1 -1
  131. package/dist/pages.es.js +1 -1
  132. package/dist/rich-text-editor-BmsjY03B.js +2949 -0
  133. package/dist/rich-text-editor-GS2kpTAK.cjs +2966 -0
  134. package/dist/sidebar-CVUGHOS_.cjs +756 -0
  135. package/dist/sidebar-CmvwjnVb.js +757 -0
  136. package/dist/ui.cjs.js +12 -2
  137. package/dist/ui.es.js +24 -14
  138. package/dist/use-audio-player-Bkh23vQ3.js +177 -0
  139. package/dist/use-audio-player-Dn1NR9xN.cjs +176 -0
  140. package/dist/utils/color-utils.d.ts +51 -0
  141. package/dist/xertica-assistant-BMqdyRVi.js +2082 -0
  142. package/dist/xertica-assistant-Bj3vBCq_.cjs +2081 -0
  143. package/dist/xertica-ui.css +1 -1
  144. package/docs/ai-usage.md +28 -10
  145. package/docs/architecture-improvements.md +463 -0
  146. package/docs/architecture.md +77 -1
  147. package/docs/components/assistant-chart.md +1 -1
  148. package/docs/components/assistant.md +159 -0
  149. package/docs/components/audio-player.md +46 -0
  150. package/docs/components/branding.md +251 -0
  151. package/docs/components/chart.md +354 -39
  152. package/docs/components/code-block.md +108 -0
  153. package/docs/components/file-upload.md +119 -2
  154. package/docs/components/formatted-document.md +113 -0
  155. package/docs/components/hooks.md +430 -0
  156. package/docs/components/image-with-fallback.md +106 -0
  157. package/docs/components/map-layers.md +140 -0
  158. package/docs/components/modern-chat-input.md +163 -0
  159. package/docs/components/pages.md +351 -0
  160. package/docs/components/pagination.md +187 -0
  161. package/docs/components/rich-text-editor.md +164 -0
  162. package/docs/components/sidebar.md +153 -4
  163. package/docs/components/stepper.md +157 -12
  164. package/docs/components/tree-view.md +164 -6
  165. package/docs/doc-audit.md +223 -0
  166. package/docs/getting-started.md +155 -1
  167. package/docs/guidelines.md +14 -8
  168. package/docs/layout.md +2 -2
  169. package/docs/llms.md +29 -9
  170. package/docs/patterns/detail-page.md +276 -0
  171. package/docs/patterns/settings.md +346 -0
  172. package/docs/patterns/wizard.md +217 -0
  173. package/guidelines/Guidelines.md +5 -3
  174. package/llms.txt +1 -1
  175. package/package.json +10 -10
  176. package/styles/xertica/tokens.css +41 -12
  177. package/templates/CLAUDE.md +16 -6
  178. package/templates/guidelines/Guidelines.md +16 -4
  179. package/templates/package.json +3 -3
  180. package/templates/src/styles/xertica/tokens.css +39 -10
  181. package/utils/color-utils.ts +72 -0
@@ -1,248 +1,64 @@
1
- import React, { useState, useEffect, useRef } from 'react';
1
+ import React from 'react';
2
+ import { useAssistant } from './use-assistant';
2
3
  import { motion, AnimatePresence } from 'framer-motion';
3
- import {
4
- MessageSquare,
5
- Heart,
6
- History,
7
- MoreHorizontal,
8
- X,
9
- ChevronLeft,
10
- ChevronRight,
11
- PanelRight,
12
- Plus,
13
- Copy,
14
- Check,
15
- FileText,
16
- Music,
17
- Image as ImageIcon,
18
- Radio,
19
- Loader2,
20
- Edit,
21
- Download,
22
- Search,
23
- Folder,
24
- Users,
25
- ExternalLink,
26
- Star,
27
- Clock,
28
- FolderOpen,
29
- Filter,
30
- Mail,
31
- BarChart3,
32
- Bookmark,
33
- Maximize2,
34
- AlertCircle,
35
- ThumbsUp,
36
- ThumbsDown,
37
- Table as TableIcon,
38
- ArrowLeft,
39
- Bold,
40
- Italic,
41
- Underline,
42
- AlignLeft,
43
- AlignCenter,
44
- AlignRight,
45
- List,
46
- Type
47
- } from 'lucide-react';
48
- import {
49
- Bar,
50
- BarChart,
51
- CartesianGrid,
52
- XAxis,
53
- Tooltip as RechartsTooltip,
54
- ResponsiveContainer
55
- } from "recharts";
56
- import {
57
- ChartConfig,
58
- ChartContainer,
59
- ChartTooltip,
60
- ChartTooltipContent,
61
- ChartLegend,
62
- ChartLegendContent
63
- } from '../../ui/chart';
64
- import {
65
- Table,
66
- TableBody,
67
- TableCaption,
68
- TableCell,
69
- TableHead,
70
- TableHeader,
71
- TableRow,
72
- } from '../../ui/table';
73
- import {
74
- Dialog,
75
- DialogContent,
76
- DialogDescription,
77
- DialogFooter,
78
- DialogHeader,
79
- DialogTitle,
80
- } from '../../ui/dialog';
81
- import {
82
- DropdownMenu,
83
- DropdownMenuContent,
84
- DropdownMenuItem,
85
- DropdownMenuTrigger,
86
- } from '../../ui/dropdown-menu';
87
- import { Textarea } from '../../ui/textarea';
88
- import { Button } from '../../ui/button';
89
4
  import { ScrollArea } from '../../ui/scroll-area';
90
- import { Separator } from '../../ui/separator';
91
- import { RichTextEditor } from '../../ui/rich-text-editor';
92
- import { MarkdownMessage } from '../markdown-message';
93
- import { FormattedDocument } from '../formatted-document';
94
5
  import { ModernChatInput } from '../modern-chat-input';
95
- import type { ActionType } from '../modern-chat-input';
96
6
  import { XerticaOrbe } from '../../brand/xertica-orbe';
97
- import { Tooltip, TooltipTrigger } from '../../ui/tooltip';
98
- import * as TooltipPrimitive from '@radix-ui/react-tooltip';
7
+ import { Button } from '../../ui/button';
99
8
  import { cn } from '../../shared/utils';
100
- import { toast } from 'sonner';
101
9
 
102
- // Tooltip customizado estilo sidebar (branco)
103
- function AssistantTooltipContent({
104
- className,
105
- sideOffset = 0,
106
- children,
107
- ...props
108
- }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
109
- return (
110
- <TooltipPrimitive.Portal>
111
- <TooltipPrimitive.Content
112
- data-slot="tooltip-content"
113
- sideOffset={sideOffset}
114
- className={cn(
115
- "bg-popover text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
116
- className,
117
- )}
118
- {...props}
119
- >
120
- {children}
121
- <TooltipPrimitive.Arrow
122
- className="fill-popover z-50 drop-shadow-sm"
123
- width={8}
124
- height={4}
125
- />
126
- </TooltipPrimitive.Content>
127
- </TooltipPrimitive.Portal>
128
- );
129
- }
10
+ // Sub-components
11
+ import {
12
+ AssistantCollapsedView,
13
+ AssistantHeader,
14
+ AssistantTabBar,
15
+ AssistantWelcomeScreen,
16
+ AssistantMessageBubble,
17
+ AssistantTypingIndicator,
18
+ AssistantConversationList,
19
+ AssistantFeedbackDialog,
20
+ AssistantDocumentEditor,
21
+ } from './parts';
22
+ import type { EvaluationState } from './parts';
130
23
 
131
24
  // ============================================================================
132
- // TypeScript Interfaces & Types
25
+ // TypeScript Interfaces & Types — imported from shared types.ts and re-exported
26
+ // for backward compatibility with consumers that import from xertica-assistant.
133
27
  // ============================================================================
134
-
135
- /**
136
- * Supported message sender types in the assistant
137
- */
138
- export type MessageType = 'user' | 'assistant' | 'system';
139
-
140
- /**
141
- * Supported attachment types that can be included in messages
142
- */
143
- export type AttachmentType = 'file' | 'audio' | 'image' | 'video' | 'document' | 'podcast' | 'search';
144
-
145
- /**
146
- * Supported search result categories
147
- */
148
- export type SearchResultType = 'document' | 'project' | 'conversation' | 'file' | 'contact';
149
-
150
- /**
151
- * Individual search result item returned by an assistant search query
152
- */
153
- export interface SearchResult {
154
- id: string;
155
- type: SearchResultType;
156
- title: string;
157
- description: string;
158
- path: string;
159
- relevance: number;
160
- lastModified?: string;
161
- }
162
-
163
- /**
164
- * Metadata about a search source (e.g., a document repository or knowledge base)
165
- */
166
- export interface SearchSource {
167
- name: string;
168
- count: number;
169
- }
170
-
171
- /**
172
- * A quick-action command shown below a search result message
173
- */
174
- export interface SearchCommand {
175
- id: string;
176
- icon: string;
177
- label: string;
178
- description: string;
179
- }
180
-
181
- /**
182
- * A single chat message in the assistant conversation
183
- */
184
- export interface Message {
185
- id: string;
186
- type: MessageType;
187
- content: string;
188
- timestamp: Date;
189
- isFavorite?: boolean;
190
- attachmentType?: AttachmentType;
191
- attachmentName?: string;
192
- documentContent?: string;
193
- documentTitle?: string;
194
- audioUrl?: string;
195
- searchResults?: SearchResult[];
196
- searchSources?: SearchSource[];
197
- searchCommands?: SearchCommand[];
198
- chartData?: any[];
199
- chartConfig?: any;
200
- tableData?: {
201
- caption?: string;
202
- headers: string[];
203
- rows: (string | React.ReactNode)[][];
204
- };
205
- evaluation?: 'like' | 'dislike';
206
- evaluationReason?: string;
207
- }
208
-
209
- /**
210
- * A saved conversation entry shown in the history panel
211
- */
212
- export interface Conversation {
213
- id: string;
214
- title: string;
215
- lastMessage?: string;
216
- timestamp: string;
217
- isFavorite: boolean;
218
- messages: Message[];
219
- }
220
-
221
- /**
222
- * A suggested prompt shown to the user in the chat input area
223
- */
224
- export interface Suggestion {
225
- id: string;
226
- text: string;
227
- icon?: React.ReactNode;
228
- }
229
-
230
- /**
231
- * Layout mode for the assistant panel
232
- */
233
- export type AssistantMode = 'collapsed' | 'expanded' | 'fullPage';
234
-
235
- /**
236
- * Available tabs in the assistant panel
237
- */
238
- export type AssistantTab = 'chat' | 'historico' | 'favoritos';
28
+ import type {
29
+ MessageType,
30
+ AttachmentType,
31
+ SearchResultType,
32
+ AssistantMode,
33
+ AssistantTab,
34
+ SearchResult,
35
+ SearchSource,
36
+ SearchCommand,
37
+ Message,
38
+ Conversation,
39
+ Suggestion,
40
+ MockResponse,
41
+ } from './types';
42
+
43
+ export type {
44
+ MessageType,
45
+ AttachmentType,
46
+ SearchResultType,
47
+ AssistantMode,
48
+ AssistantTab,
49
+ SearchResult,
50
+ SearchSource,
51
+ SearchCommand,
52
+ Message,
53
+ Conversation,
54
+ Suggestion,
55
+ MockResponse,
56
+ };
239
57
 
240
58
  // ============================================================================
241
59
  // Component Props
242
60
  // ============================================================================
243
61
 
244
- import { gerarResposta, type MockResponse } from '../../shared/assistant-utils';
245
-
246
62
  export interface XerticaAssistantProps {
247
63
  /**
248
64
  * Layout mode for the assistant panel
@@ -277,8 +93,6 @@ export interface XerticaAssistantProps {
277
93
  */
278
94
  customResponses?: MockResponse[];
279
95
 
280
-
281
-
282
96
  /**
283
97
  * Callback fired when the user navigates to the settings page
284
98
  */
@@ -310,6 +124,12 @@ export interface XerticaAssistantProps {
310
124
  */
311
125
  suggestions?: Suggestion[];
312
126
 
127
+ /**
128
+ * Subtitle shown below the user's name in the empty state.
129
+ * @default 'Como posso ajudar?'
130
+ */
131
+ welcomeMessage?: string;
132
+
313
133
  /**
314
134
  * Callback fired when the user sends a message
315
135
  */
@@ -480,6 +300,7 @@ export function XerticaAssistant({
480
300
  customResponses = [],
481
301
  responseGenerator,
482
302
  richSuggestions = [],
303
+ welcomeMessage = 'Como posso ajudar?',
483
304
  onRichAction,
484
305
  onEvaluation,
485
306
  feedbackOptions,
@@ -493,368 +314,68 @@ export function XerticaAssistant({
493
314
  enableSearch = true,
494
315
  }: XerticaAssistantProps) {
495
316
  // ============================================================================
496
- // State Management
497
- // ============================================================================
498
-
499
- const isFullPage = mode === 'fullPage';
500
- const [internalIsExpanded, setInternalIsExpanded] = useState(controlledIsExpanded ?? true);
501
- const isExpanded = controlledIsExpanded ?? internalIsExpanded;
502
-
503
- const [isMobile, setIsMobile] = useState(false);
504
- useEffect(() => {
505
- const handleResize = () => setIsMobile(window.innerWidth < 768);
506
- handleResize();
507
- window.addEventListener('resize', handleResize);
508
- return () => window.removeEventListener('resize', handleResize);
509
- }, []);
510
-
511
- const [abaSelecionada, setAbaSelecionada] = useState<AssistantTab>(defaultTab);
512
- const [mensagens, setMensagens] = useState<Message[]>(initialMessages);
513
- const [mensagem, setMensagem] = useState('');
514
- const [conversas, setConversas] = useState<Conversation[]>(savedConversations);
515
- const [conversaAtual, setConversaAtual] = useState<string | null>(null);
516
- const [copiedId, setCopiedId] = useState<string | null>(null);
517
- const [generatingPodcastId, setGeneratingPodcastId] = useState<string | null>(null);
518
- const [executingCommand, setExecutingCommand] = useState<string | null>(null);
519
- const [savedSearches, setSavedSearches] = useState<string[]>([]);
520
- const [editingDocument, setEditingDocument] = useState<{
521
- content: string;
522
- title: string;
523
- } | null>(null);
524
- const [showMoreSuggestions, setShowMoreSuggestions] = useState(false);
525
- const [evaluationState, setEvaluationState] = useState<{
526
- isOpen: boolean;
527
- messageId: string | null;
528
- type: 'dislike' | null;
529
- category?: string | null;
530
- reason: string;
531
- }>({
532
- isOpen: false,
533
- messageId: null,
534
- type: null,
535
- category: null,
536
- reason: ''
537
- });
538
-
539
- // ============================================================================
540
- // Refs
541
- // ============================================================================
542
-
543
- const messagesEndRef = useRef<HTMLDivElement>(null);
544
- const fileInputRef = useRef<HTMLInputElement>(null);
545
- const audioInputRef = useRef<HTMLInputElement>(null);
546
-
547
- // ============================================================================
548
- // Sugestões padrão
549
- // ============================================================================
550
-
551
- const defaultSuggestions: Suggestion[] = [
552
- { id: '1', text: 'Me ajude a criar um documento profissional' },
553
- { id: '2', text: 'Buscar nos meus arquivos por "relatório"' },
554
- { id: '3', text: 'Resuma as conversas importantes desta semana' },
555
- { id: '4', text: 'Crie um podcast sobre o último projeto' },
556
- ];
557
-
558
- const sugestoes = propSuggestions ?? defaultSuggestions;
559
-
560
- // API Key Validation removed
561
-
562
-
563
- // ============================================================================
564
- // Effects
565
- // ============================================================================
566
-
567
- // Auto-scroll ao adicionar mensagens
568
- useEffect(() => {
569
- if (messagesEndRef.current && abaSelecionada === 'chat') {
570
- messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
571
- }
572
- }, [mensagens, abaSelecionada]);
573
-
574
- // Sincronizar mensagens iniciais apenas quando elas mudarem e não forem vazias
575
- useEffect(() => {
576
- if (initialMessages && initialMessages.length > 0) {
577
- setMensagens(initialMessages);
578
- }
579
- }, [initialMessages]);
580
-
581
- // ============================================================================
582
- // Handlers
583
- // ============================================================================
584
-
585
- const handleToggle = () => {
586
- if (onToggle) {
587
- onToggle();
588
- } else {
589
- setInternalIsExpanded(!internalIsExpanded);
590
- }
591
- };
592
-
593
- const handleExpandWithTab = (tab: AssistantTab) => {
594
- setAbaSelecionada(tab);
595
- if (onToggle) {
596
- onToggle();
597
- } else {
598
- setInternalIsExpanded(true);
599
- }
600
- };
601
-
602
- const handleEnviarMensagem = async (arg?: string | ActionType) => {
603
- let msgToSend = mensagem;
604
-
605
- // Check if argument is a message text (from simple string) or ActionType
606
- if (typeof arg === 'string' && !['document', 'podcast', 'search'].includes(arg)) {
607
- msgToSend = arg;
608
- }
609
-
610
- if (!msgToSend.trim() || isProcessing) return;
611
-
612
- const novaMensagem: Message = {
613
- id: `msg-${Date.now()}`,
614
- type: 'user',
615
- content: msgToSend,
616
- timestamp: new Date(),
617
- isFavorite: false,
618
- };
619
-
620
- setMensagens(prev => [...prev, novaMensagem]);
621
-
622
-
623
- if (onSendMessage) {
624
- onSendMessage(msgToSend);
625
- }
626
-
627
- if (demoMode || responseGenerator) {
628
- const mensagemAtual = msgToSend;
629
- setTimeout(async () => {
630
- let resposta: string | Partial<Message>;
631
-
632
- if (responseGenerator) {
633
- resposta = await responseGenerator(mensagemAtual);
634
- } else {
635
- resposta = gerarResposta(mensagemAtual, customResponses);
636
- }
637
-
638
- let novaMensagemIA: Message = {
639
- id: `msg-${Date.now()}-ia`,
640
- type: 'assistant',
641
- content: '',
642
- timestamp: new Date(),
643
- isFavorite: false
644
- };
645
-
646
- if (typeof resposta === 'string') {
647
- novaMensagemIA.content = resposta;
648
- } else {
649
- novaMensagemIA = { ...novaMensagemIA, ...resposta };
650
- }
651
-
652
- setMensagens(prev => [...prev, novaMensagemIA]);
653
- }, 1000 + Math.random() * 1000);
654
- }
655
-
656
- setMensagem('');
657
- };
658
-
659
- const handleToggleFavorite = (messageId: string) => {
660
- setMensagens(prev =>
661
- prev.map(msg =>
662
- msg.id === messageId ? { ...msg, isFavorite: !msg.isFavorite } : msg
663
- )
664
- );
665
- };
666
-
667
- const handleCopyMessage = async (content: string, messageId: string) => {
668
- try {
669
- // Try modern Clipboard API first
670
- if (navigator.clipboard && navigator.clipboard.writeText) {
671
- try {
672
- await navigator.clipboard.writeText(content);
673
- setCopiedId(messageId);
674
- setTimeout(() => setCopiedId(null), 2000);
675
- } catch (clipboardError: any) {
676
- // If clipboard API fails due to permissions, fall back to execCommand
677
- if (clipboardError.name === 'NotAllowedError' || clipboardError.message.includes('permissions policy')) {
678
- throw new Error('Clipboard permission denied, falling back');
679
- }
680
- throw clipboardError;
681
- }
682
- } else {
683
- throw new Error('Clipboard API not available');
684
- }
685
- } catch (err) {
686
- // Fallback for browsers that don't support Clipboard API or when permissions are denied
687
- try {
688
- const textArea = document.createElement('textarea');
689
- textArea.value = content;
690
- textArea.style.position = 'fixed';
691
- textArea.style.left = '-999999px';
692
- textArea.style.top = '-999999px';
693
- document.body.appendChild(textArea);
694
- textArea.focus();
695
- textArea.select();
696
-
697
- const successful = document.execCommand('copy');
698
- document.body.removeChild(textArea);
699
-
700
- if (successful) {
701
- setCopiedId(messageId);
702
- setTimeout(() => setCopiedId(null), 2000);
703
- } else {
704
- console.error('Falha ao copiar: execCommand returned false');
705
- }
706
- } catch (fallbackErr) {
707
- console.error('Falha ao copiar:', fallbackErr);
708
- }
709
- }
710
- };
711
-
712
- const handleGeneratePodcast = async (messageId: string, content: string) => {
713
- setGeneratingPodcastId(messageId);
714
-
715
- // Simulação de geração de podcast
716
- setTimeout(() => {
717
- setMensagens(prev =>
718
- prev.map(msg =>
719
- msg.id === messageId
720
- ? {
721
- ...msg,
722
- attachmentType: 'podcast',
723
- attachmentName: 'Podcast - ' + content.substring(0, 30) + '...',
724
- audioUrl: 'data:audio/mpeg;base64,//uQx...' // Mock
725
- }
726
- : msg
727
- )
728
- );
729
- setGeneratingPodcastId(null);
730
- }, 2000);
731
- };
732
-
733
- const handleDownloadDocument = (content: string, fileName: string) => {
734
- const blob = new Blob([content], { type: 'text/markdown' });
735
- const url = URL.createObjectURL(blob);
736
- const a = document.createElement('a');
737
- a.href = url;
738
- a.download = fileName.endsWith('.md') ? fileName : fileName + '.md';
739
- document.body.appendChild(a);
740
- a.click();
741
- document.body.removeChild(a);
742
- URL.revokeObjectURL(url);
743
- };
744
-
745
- const handleDownloadPodcast = (audioUrl: string, fileName: string) => {
746
- const a = document.createElement('a');
747
- a.href = audioUrl;
748
- a.download = fileName.endsWith('.mp3') ? fileName : fileName + '.mp3';
749
- document.body.appendChild(a);
750
- a.click();
751
- document.body.removeChild(a);
752
- };
753
-
754
- const handleEditDocument = (content: string, title: string) => {
755
- setEditingDocument({ content, title });
756
- };
757
-
758
- const handleNovaConversa = () => {
759
- setMensagens([]);
760
- setConversaAtual(null);
761
- setAbaSelecionada('chat');
762
- };
763
-
764
- const handleSelecionarConversa = (conversaId: string) => {
765
- const conversa = conversas.find(c => c.id === conversaId);
766
- if (conversa) {
767
- setMensagens(conversa.messages);
768
- setConversaAtual(conversaId);
769
- setAbaSelecionada('chat');
770
- }
771
- };
772
-
773
- const handleToggleFavoritaConversa = (conversaId: string) => {
774
- setConversas(prev =>
775
- prev.map(conv =>
776
- conv.id === conversaId ? { ...conv, isFavorite: !conv.isFavorite } : conv
777
- )
778
- );
779
- };
780
-
781
- const handleOpenSearchResult = (result: SearchResult) => {
782
- console.log('Abrir resultado:', result);
783
- // Implementar lógica de navegação ou abertura de resultado
784
- };
785
-
786
- const handleExecuteSearchCommand = async (
787
- commandId: string,
788
- searchTerm: string,
789
- results: SearchResult[],
790
- messageId: string
791
- ) => {
792
- setExecutingCommand(commandId);
793
-
794
- // Simular execução de comando
795
- setTimeout(() => {
796
- if (commandId === '5') {
797
- setSavedSearches(prev => [...prev, messageId]);
798
- }
799
- setExecutingCommand(null);
800
- }, 1500);
801
- };
802
-
803
- const handleRichSuggestionClick = (suggestion: Suggestion) => {
804
- if (onRichAction) {
805
- onRichAction(suggestion.id, suggestion.text);
806
- } else {
807
- // Fallback: send as message
808
- handleEnviarMensagem(suggestion.text);
809
- }
810
- setShowMoreSuggestions(false);
811
- };
812
-
813
- const handleEvaluationClick = (messageId: string, type: 'like' | 'dislike') => {
814
- if (type === 'like') {
815
- if (onEvaluation) onEvaluation(messageId, 'like');
816
- // Update local state
817
- setMensagens(prev => prev.map(m => m.id === messageId ? { ...m, evaluation: 'like' } : m));
818
- toast.success("Obrigado por avaliar positivamente!");
819
- }
820
- };
821
-
822
- const openFeedbackDialog = (messageId: string, category: string | null) => {
823
- setEvaluationState({
824
- isOpen: true,
825
- messageId,
826
- type: 'dislike',
827
- category,
828
- reason: ''
829
- });
830
- };
831
-
832
- const handleSubmitDislike = () => {
833
- if (evaluationState.messageId) {
834
- const finalReason = evaluationState.category
835
- ? (evaluationState.reason ? `${evaluationState.category}: ${evaluationState.reason}` : evaluationState.category)
836
- : evaluationState.reason;
837
-
838
- if (onEvaluation) {
839
- onEvaluation(evaluationState.messageId, 'dislike', finalReason);
840
- }
841
-
842
- // Update local state
843
- setMensagens(prev => prev.map(m => m.id === evaluationState.messageId ? { ...m, evaluation: 'dislike', evaluationReason: finalReason } : m));
844
- toast.success("Obrigado pelo seu feedback. Vamos melhorar.");
845
- }
846
- setEvaluationState({ isOpen: false, messageId: null, type: null, category: null, reason: '' });
847
- };
848
-
849
- // ============================================================================
850
- // Computations
317
+ // State & Logic — delegated to useAssistant hook
851
318
  // ============================================================================
852
319
 
853
- const conversasFiltradas = conversas.filter(conversa => {
854
- if (abaSelecionada === 'favoritos') {
855
- return conversa.isFavorite;
856
- }
857
- return true;
320
+ const {
321
+ isFullPage,
322
+ isExpanded,
323
+ isMobile,
324
+ abaSelecionada,
325
+ setAbaSelecionada,
326
+ mensagens,
327
+ mensagem,
328
+ setMensagem,
329
+ conversaAtual,
330
+ conversasFiltradas,
331
+ copiedId,
332
+ generatingPodcastId,
333
+ executingCommand,
334
+ savedSearches,
335
+ editingDocument,
336
+ setEditingDocument,
337
+ showMoreSuggestions,
338
+ setShowMoreSuggestions,
339
+ evaluationState,
340
+ setEvaluationState,
341
+ sugestoes,
342
+ messagesEndRef,
343
+ fileInputRef,
344
+ audioInputRef,
345
+ handleToggle,
346
+ handleExpandWithTab,
347
+ handleEnviarMensagem,
348
+ handleToggleFavorite,
349
+ handleCopyMessage,
350
+ handleGeneratePodcast,
351
+ handleDownloadDocument,
352
+ handleDownloadPodcast,
353
+ handleEditDocument,
354
+ handleNovaConversa,
355
+ handleSelecionarConversa,
356
+ handleToggleFavoritaConversa,
357
+ handleOpenSearchResult,
358
+ handleExecuteSearchCommand,
359
+ handleRichSuggestionClick,
360
+ handleEvaluationClick,
361
+ openFeedbackDialog,
362
+ handleSubmitDislike,
363
+ } = useAssistant({
364
+ mode,
365
+ isExpanded: controlledIsExpanded,
366
+ onToggle,
367
+ defaultTab,
368
+ demoMode,
369
+ customResponses,
370
+ initialMessages,
371
+ savedConversations,
372
+ suggestions: propSuggestions,
373
+ onSendMessage,
374
+ isProcessing,
375
+ responseGenerator,
376
+ richSuggestions,
377
+ onRichAction,
378
+ onEvaluation,
858
379
  });
859
380
 
860
381
  // ============================================================================
@@ -865,11 +386,12 @@ export function XerticaAssistant({
865
386
  isFullPage
866
387
  ? '100%'
867
388
  : isExpanded
868
- ? (editingDocument ? '420px' : '420px')
389
+ ? '420px'
869
390
  : '80px' // Compacto quando fechado - apenas ícones
870
391
  );
871
392
  const containerHeight = height ?? 'h-full';
872
393
 
394
+ // Mobile floating button — shown when collapsed on mobile
873
395
  if (isMobile && !isExpanded) {
874
396
  return (
875
397
  <Button
@@ -927,195 +449,37 @@ export function XerticaAssistant({
927
449
  width: isFullPage ? '100%' : containerWidth,
928
450
  } : undefined}
929
451
  >
930
- {/* Document Editor Overlay */}
931
- {editingDocument && !isFullPage && (
932
- <div
933
- className={cn(
934
- "flex flex-col border-border bg-background overflow-hidden transition-all duration-300",
935
- isMobile ? "fixed inset-0 z-[110] w-full h-[100dvh]" : "absolute top-0 bottom-0 right-full w-[800px] border-l border-r border-border shadow-[-25px_0_50px_-15px_rgba(0,0,0,0.15)] z-[90]"
936
- )}
937
- style={{ maxWidth: isMobile ? 'none' : `calc(100vw - ${containerWidth})` }}
938
- >
939
- <div className="h-full flex flex-col">
940
- {/* Header Toolbar */}
941
- <div className="px-4 h-[64px] border-b border-border bg-card flex items-center justify-between">
942
- <div className="flex items-center gap-3">
943
- <div className="p-2 bg-primary/10 rounded-md">
944
- <FileText className="w-4 h-4 text-primary" />
945
- </div>
946
- <h3 className="font-semibold text-card-foreground text-sm">{editingDocument.title}</h3>
947
- </div>
948
- <Button
949
- variant="ghost"
950
- size="icon"
951
- className="h-8 w-8 rounded-full"
952
- onClick={() => setEditingDocument(null)}
953
- aria-label="Fechar editor de documento"
954
- >
955
- <X className="w-4 h-4" />
956
- </Button>
957
- </div>
958
-
959
- {/* Rich Text Editor */}
960
- <div className="flex-1 bg-muted/20 p-0 sm:p-6 overflow-hidden">
961
- <RichTextEditor
962
- value={editingDocument.content || ''}
963
- onChange={(newVal) => setEditingDocument(prev => prev ? { ...prev, content: newVal } : null)}
964
- placeholder="Comece a digitar o conteúdo do seu documento aqui..."
965
- className="max-w-4xl mx-auto h-full"
966
- actionButton={
967
- <Button size="sm" className="gap-2 h-8">
968
- <Check className="w-3.5 h-3.5" />
969
- Salvar
970
- </Button>
971
- }
972
- />
973
- </div>
974
- </div>
975
- </div>
976
- )}
977
- {/* Header - Toggle Button - Apenas visível quando não está em fullPage */}
978
- {!isFullPage && (
979
- <div
980
- className={cn(
981
- "border-b border-border flex items-center h-[64px]",
982
- isExpanded ? "justify-between px-4" : "justify-center px-0"
983
- )}
984
- >
985
- {isExpanded && (
986
- <motion.div
987
- initial={{ opacity: 0, x: -10 }}
988
- animate={{ opacity: 1, x: 0 }}
989
- exit={{ opacity: 0, x: -10 }}
990
- className="flex items-center gap-2 overflow-hidden"
991
- >
992
- <div className="flex-shrink-0">
993
- <XerticaOrbe size={32} />
994
- </div>
995
- <span className="text-foreground font-medium truncate">Assistente Xertica</span>
996
- </motion.div>
997
- )}
998
-
999
- <div className="flex items-center gap-1">
1000
- {isExpanded && onNavigateFullPage && (
1001
- <Button
1002
- variant="ghost"
1003
- size="sm"
1004
- onClick={onNavigateFullPage}
1005
- className="h-8 w-8 p-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground rounded-full"
1006
- title="Expandir assistente"
1007
- aria-label="Expandir assistente"
1008
- >
1009
- <Maximize2 className="w-4 h-4" />
1010
- </Button>
1011
- )}
452
+ {/* Document Editor Overlay */}
453
+ {editingDocument && !isFullPage && (
454
+ <AssistantDocumentEditor
455
+ document={editingDocument}
456
+ isMobile={isMobile}
457
+ containerWidth={containerWidth}
458
+ onClose={() => setEditingDocument(null)}
459
+ onChange={(doc) => setEditingDocument(doc)}
460
+ />
461
+ )}
1012
462
 
1013
- <Button
1014
- variant="ghost"
1015
- size="sm"
1016
- onClick={handleToggle}
1017
- className={cn(
1018
- "h-8 w-8 p-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground rounded-full",
1019
- !isExpanded && "w-10 h-10" // Slightly larger touch target when collapsed if desired, or keep consistent
1020
- )}
1021
- aria-label={isExpanded ? "Recolher assistente" : "Expandir assistente"}
1022
- >
1023
- {isExpanded ? (
1024
- <ChevronRight className="w-4 h-4" />
1025
- ) : (
1026
- <PanelRight className="w-4 h-4" />
1027
- )}
1028
- </Button>
1029
- </div>
1030
- </div>
463
+ {/* Header — only visible when not in fullPage mode */}
464
+ {!isFullPage && (
465
+ <AssistantHeader
466
+ isExpanded={isExpanded}
467
+ onToggle={handleToggle}
468
+ onNavigateFullPage={onNavigateFullPage}
469
+ />
1031
470
  )}
1032
471
 
1033
- {/* Collapsed State - Icons Only */}
472
+ {/* Collapsed State Icons Only */}
1034
473
  {!isExpanded && !isFullPage && (
1035
- <div className="flex flex-col items-center p-4 space-y-4">
1036
- {/* Ícone do Assistente - Xertica Orbe */}
1037
- <Tooltip>
1038
- <TooltipTrigger asChild>
1039
- <button
1040
- onClick={handleToggle}
1041
- className="w-10 h-10 rounded-full flex items-center justify-center hover:bg-accent/50 transition-colors duration-200 cursor-pointer mx-auto"
1042
- aria-label="Abrir assistente"
1043
- >
1044
- <XerticaOrbe size={32} />
1045
- </button>
1046
- </TooltipTrigger>
1047
- <AssistantTooltipContent
1048
- side="left"
1049
- sideOffset={8}
1050
- >
1051
- <p>Assistente Xertica</p>
1052
- </AssistantTooltipContent>
1053
- </Tooltip>
1054
-
1055
- <Tooltip>
1056
- <TooltipTrigger asChild>
1057
- <Button
1058
- variant="ghost"
1059
- size="sm"
1060
- onClick={() => handleExpandWithTab('chat')}
1061
- className="w-8 h-8 p-0 text-muted-foreground"
1062
- >
1063
- <MessageSquare className="w-4 h-4" />
1064
- </Button>
1065
- </TooltipTrigger>
1066
- <AssistantTooltipContent
1067
- side="left"
1068
- sideOffset={8}
1069
- >
1070
- <p>Chat</p>
1071
- </AssistantTooltipContent>
1072
- </Tooltip>
1073
-
1074
- {showFavorites && (
1075
- <Tooltip>
1076
- <TooltipTrigger asChild>
1077
- <Button
1078
- variant="ghost"
1079
- size="sm"
1080
- onClick={() => handleExpandWithTab('favoritos')}
1081
- className="w-8 h-8 p-0 text-muted-foreground"
1082
- >
1083
- <Heart className="w-4 h-4" />
1084
- </Button>
1085
- </TooltipTrigger>
1086
- <AssistantTooltipContent
1087
- side="left"
1088
- sideOffset={8}
1089
- >
1090
- <p>Favoritos</p>
1091
- </AssistantTooltipContent>
1092
- </Tooltip>
1093
- )}
1094
-
1095
- {showHistory && (
1096
- <Tooltip>
1097
- <TooltipTrigger asChild>
1098
- <Button
1099
- variant="ghost"
1100
- size="sm"
1101
- onClick={() => handleExpandWithTab('historico')}
1102
- className="w-8 h-8 p-0 text-muted-foreground"
1103
- >
1104
- <History className="w-4 h-4" />
1105
- </Button>
1106
- </TooltipTrigger>
1107
- <AssistantTooltipContent
1108
- side="left"
1109
- sideOffset={8}
1110
- >
1111
- <p>Histórico</p>
1112
- </AssistantTooltipContent>
1113
- </Tooltip>
1114
- )}
1115
- </div>
474
+ <AssistantCollapsedView
475
+ showHistory={showHistory}
476
+ showFavorites={showFavorites}
477
+ onToggle={handleToggle}
478
+ onExpandWithTab={handleExpandWithTab}
479
+ />
1116
480
  )}
1117
481
 
1118
- {/* Expanded State - Full Content */}
482
+ {/* Expanded State Full Content */}
1119
483
  <AnimatePresence>
1120
484
  {(isExpanded || isFullPage) && (
1121
485
  <motion.div
@@ -1125,600 +489,61 @@ export function XerticaAssistant({
1125
489
  transition={{ duration: 0.2 }}
1126
490
  className="flex-1 flex flex-col overflow-hidden"
1127
491
  >
1128
- {/* Navigation Tabs - Oculto em modo fullPage e quando não abas alternativas */}
492
+ {/* Navigation Tabs hidden in fullPage mode and when no secondary tabs are enabled */}
1129
493
  {!isFullPage && (showHistory || showFavorites) && (
1130
- <div
1131
- className="px-4 py-2 border-b border-border"
1132
- >
1133
- <div className="flex gap-1">
1134
- <Button
1135
- variant={abaSelecionada === 'chat' ? 'default' : 'ghost'}
1136
- size="sm"
1137
- onClick={() => setAbaSelecionada('chat')}
1138
- className="flex-1 h-8"
1139
- aria-label="Ver chat"
1140
- >
1141
- <MessageSquare className="w-3 h-3 mr-1" />
1142
- Chat
1143
- </Button>
1144
- {showHistory && (
1145
- <Button
1146
- variant={abaSelecionada === 'historico' ? 'default' : 'ghost'}
1147
- size="sm"
1148
- onClick={() => setAbaSelecionada('historico')}
1149
- className="flex-1 h-8"
1150
- aria-label="Ver histórico de conversas"
1151
- >
1152
- <History className="w-3 h-3 mr-1" />
1153
- Histórico
1154
- </Button>
1155
- )}
1156
- {showFavorites && (
1157
- <Button
1158
- variant={abaSelecionada === 'favoritos' ? 'default' : 'ghost'}
1159
- size="sm"
1160
- onClick={() => setAbaSelecionada('favoritos')}
1161
- className="flex-1 h-8"
1162
- aria-label="Ver mensagens favoritas"
1163
- >
1164
- <Heart className="w-3 h-3 mr-1" />
1165
- Favoritos
1166
- </Button>
1167
- )}
1168
- </div>
1169
- </div>
494
+ <AssistantTabBar
495
+ activeTab={abaSelecionada}
496
+ showHistory={showHistory}
497
+ showFavorites={showFavorites}
498
+ onTabChange={setAbaSelecionada}
499
+ />
1170
500
  )}
1171
501
 
1172
502
  {/* Content Area */}
1173
503
  <div className="flex-1 overflow-hidden flex flex-col">
504
+
505
+ {/* Chat Tab */}
1174
506
  {abaSelecionada === 'chat' && (
1175
507
  <div className={`flex-1 flex flex-col min-h-0 ${isFullPage ? 'mx-auto w-full max-w-6xl' : ''}`}>
1176
- {/* API Key Warning Banner */}
1177
-
1178
-
1179
508
  {mensagens.length === 0 ? (
1180
- <div className="flex-1 overflow-y-auto min-h-0">
1181
- {/* Welcome Message */}
1182
- <div className="p-6 text-center">
1183
- <div className="mx-auto mb-4 flex items-center justify-center">
1184
- <XerticaOrbe size={64} />
1185
- </div>
1186
- <h3 className="mb-2 text-foreground">
1187
- Olá, {userName}
1188
- </h3>
1189
- <p className="text-muted-foreground">
1190
- Como posso ajudar?
1191
- </p>
1192
- </div>
1193
-
1194
- {/* Suggestions */}
1195
- <div className="px-4 pb-4 space-y-2">
1196
- {sugestoes.map((sugestao) => (
1197
- <button
1198
- key={sugestao.id}
1199
- onClick={() => handleEnviarMensagem(sugestao.text)}
1200
- className="w-full p-3 text-left rounded-[var(--radius-card)] bg-muted text-foreground transition-colors duration-200 hover:bg-muted/80"
1201
- >
1202
- {sugestao.text}
1203
- </button>
1204
- ))}
1205
-
1206
- {!showMoreSuggestions ? (
1207
- <Button
1208
- variant="ghost"
1209
- size="sm"
1210
- onClick={() => setShowMoreSuggestions(true)}
1211
- className="w-full justify-start text-muted-foreground"
1212
- >
1213
- <MoreHorizontal className="w-4 h-4 mr-2" />
1214
- Mais sugestões
1215
- </Button>
1216
- ) : (
1217
- <div className="space-y-2 pt-2 border-t border-border mt-2 animate-in slide-in-from-top-2">
1218
- {richSuggestions.length > 0 ? (
1219
- richSuggestions.map((sugestao) => (
1220
- <button
1221
- key={sugestao.id}
1222
- onClick={() => handleRichSuggestionClick(sugestao)}
1223
- className="w-full p-3 text-left rounded-[var(--radius-card)] bg-muted/50 border border-border text-foreground transition-all duration-200 hover:bg-primary/5 hover:border-primary/50 group"
1224
- >
1225
- <div className="flex items-center gap-2">
1226
- {sugestao.id.includes('chart') && <BarChart3 className="w-4 h-4 text-primary" />}
1227
- {sugestao.id.includes('table') && <TableIcon className="w-4 h-4 text-primary" />}
1228
- <span className="font-medium">{sugestao.text}</span>
1229
- </div>
1230
- </button>
1231
- ))
1232
- ) : (
1233
- <p className="text-sm text-muted-foreground text-center py-2">Nenhuma sugestão extra.</p>
1234
- )}
1235
- <Button
1236
- variant="ghost"
1237
- size="sm"
1238
- onClick={() => setShowMoreSuggestions(false)}
1239
- className="w-full justify-center text-muted-foreground mt-2"
1240
- >
1241
- <ChevronLeft className="w-4 h-4 mr-2" />
1242
- Voltar
1243
- </Button>
1244
- </div>
1245
- )}
1246
- </div>
1247
- </div>
509
+ <AssistantWelcomeScreen
510
+ userName={userName}
511
+ welcomeMessage={welcomeMessage}
512
+ suggestions={sugestoes}
513
+ richSuggestions={richSuggestions}
514
+ showMoreSuggestions={showMoreSuggestions}
515
+ onSetShowMoreSuggestions={setShowMoreSuggestions}
516
+ onSendSuggestion={handleEnviarMensagem}
517
+ onRichSuggestionClick={handleRichSuggestionClick}
518
+ />
1248
519
  ) : (
1249
520
  <ScrollArea className="flex-1 min-h-0 overflow-x-hidden [&_[data-radix-scroll-area-viewport]>div]:!block">
1250
521
  <div className="space-y-4 px-4 py-4 max-w-6xl mx-auto !block !w-full">
1251
522
  {mensagens.map((msg) => (
1252
- <motion.div
523
+ <AssistantMessageBubble
1253
524
  key={msg.id}
1254
- initial={{ opacity: 0, y: 10 }}
1255
- animate={{ opacity: 1, y: 0 }}
1256
- className={`flex gap-2 w-full min-w-0 ${msg.type === 'user' ? 'justify-end' : 'justify-start'}`}
1257
- >
1258
- {/* Avatar do Assistente */}
1259
- {msg.type === 'assistant' && (
1260
- <div className="flex-shrink-0 pt-1">
1261
- <XerticaOrbe size={32} />
1262
- </div>
1263
- )}
1264
-
1265
- <div className={cn(
1266
- "flex flex-col min-w-0 transition-all duration-300",
1267
- msg.type === 'user'
1268
- ? "max-w-[85%] md:max-w-[70%] items-end w-fit"
1269
- : cn(
1270
- "items-start",
1271
- (msg.tableData || msg.chartData || msg.searchResults || msg.attachmentType === 'document')
1272
- ? "w-full max-w-full"
1273
- : "w-fit max-w-[95%] md:max-w-[90%]"
1274
- )
1275
- )}>
1276
- <div
1277
- className={cn(
1278
- "px-4 py-2 break-words overflow-hidden overflow-x-hidden w-full min-w-0 rounded-2xl",
1279
- msg.type === 'user'
1280
- ? "bg-primary text-primary-foreground shadow-sm"
1281
- : "bg-muted text-foreground"
1282
- )}
1283
- >
1284
- {/* Document Header with Edit and Download Buttons */}
1285
- {msg.attachmentType === 'document' && (
1286
- <div
1287
- className="flex items-center justify-between mb-2 pb-2 border-b border-border min-w-0 overflow-hidden"
1288
- >
1289
- <div className="flex items-center gap-2 min-w-0 flex-1 overflow-hidden">
1290
- <FileText className="w-4 h-4 flex-shrink-0" />
1291
- <span className="text-sm font-medium break-words">{msg.attachmentName}</span>
1292
- </div>
1293
- <div className="flex items-center gap-1 flex-shrink-0">
1294
- <Button
1295
- variant="ghost"
1296
- size="sm"
1297
- onClick={() => msg.documentContent && msg.attachmentName && handleDownloadDocument(msg.documentContent, msg.attachmentName)}
1298
- className="h-6 px-2"
1299
- title="Baixar"
1300
- >
1301
- <Download className="w-3 h-3" />
1302
- </Button>
1303
- <Button
1304
- variant="ghost"
1305
- size="sm"
1306
- onClick={() => msg.documentContent && msg.documentTitle && handleEditDocument(msg.documentContent, msg.documentTitle)}
1307
- className="h-6 px-2"
1308
- >
1309
- <Edit className="w-3 h-3 mr-1" />
1310
- Editar
1311
- </Button>
1312
- </div>
1313
- </div>
1314
- )}
1315
-
1316
- {/* Attachments */}
1317
- {msg.attachmentType && msg.attachmentType !== 'podcast' && msg.attachmentType !== 'document' && (
1318
- <div
1319
- className="flex items-center gap-2 mb-2 pb-2 border-b border-border min-w-0 overflow-hidden"
1320
- >
1321
- {msg.attachmentType === 'file' && <FileText className="w-4 h-4 flex-shrink-0" />}
1322
- {msg.attachmentType === 'audio' && <Music className="w-4 h-4 flex-shrink-0" />}
1323
- {msg.attachmentType === 'image' && <ImageIcon className="w-4 h-4 flex-shrink-0" />}
1324
- <span className="text-small break-words">{msg.attachmentName}</span>
1325
- </div>
1326
- )}
1327
-
1328
- {/* Message Content */}
1329
- {msg.type === 'user' ? (
1330
- <p className="whitespace-pre-wrap break-words">{msg.content}</p>
1331
- ) : (
1332
- <>
1333
- {(msg.content.includes('🔐') || msg.content.includes('❌')) && (
1334
- <div
1335
- className="mb-3 p-3 border rounded-[var(--radius)] overflow-hidden bg-[var(--toast-error-bg)]/20 border-[var(--toast-error-border)]/30"
1336
- >
1337
- <div className="flex items-start gap-2 min-w-0">
1338
- <AlertCircle className="w-5 h-5 flex-shrink-0 mt-0.5 text-[var(--toast-error-icon)]" />
1339
- <div className="flex-1 min-w-0 overflow-hidden">
1340
- <MarkdownMessage
1341
- content={msg.content}
1342
- className="text-foreground"
1343
- />
1344
- </div>
1345
- </div>
1346
- </div>
1347
- )}
1348
- {!(msg.content.includes('🔐') || msg.content.includes('❌')) && (
1349
- <MarkdownMessage
1350
- content={msg.content}
1351
- className="text-foreground"
1352
- />
1353
- )}
1354
- </>
1355
- )}
1356
-
1357
- {/* Chart Rendering */}
1358
- {msg.chartData && msg.chartConfig && (
1359
- <div className="mt-4 w-full h-[300px] min-w-[300px]">
1360
- <ChartContainer config={msg.chartConfig} className="h-full w-full">
1361
- <BarChart accessibilityLayer data={msg.chartData}>
1362
- <CartesianGrid vertical={false} />
1363
- <XAxis
1364
- dataKey="month"
1365
- tickLine={false}
1366
- tickMargin={10}
1367
- axisLine={false}
1368
- tickFormatter={(value) => value.slice(0, 3)}
1369
- />
1370
- <ChartTooltip content={<ChartTooltipContent />} />
1371
- <ChartLegend content={<ChartLegendContent />} />
1372
- {Object.keys(msg.chartConfig).map((key) => (
1373
- <Bar key={key} dataKey={key} fill={`var(--color-${key})`} radius={4} />
1374
- ))}
1375
- </BarChart>
1376
- </ChartContainer>
1377
- </div>
1378
- )}
1379
-
1380
- {/* Table Rendering */}
1381
- {msg.tableData && (
1382
- <div className="mt-4 w-full max-w-full border rounded-[var(--radius)] overflow-x-auto custom-scrollbar relative">
1383
- <Table>
1384
- {msg.tableData.caption && <TableCaption>{msg.tableData.caption}</TableCaption>}
1385
- <TableHeader>
1386
- <TableRow>
1387
- {msg.tableData.headers.map((header, i) => (
1388
- <TableHead key={i}>{header}</TableHead>
1389
- ))}
1390
- </TableRow>
1391
- </TableHeader>
1392
- <TableBody>
1393
- {msg.tableData.rows.map((row, i) => (
1394
- <TableRow key={i}>
1395
- {row.map((cell, j) => (
1396
- <TableCell key={j}>{cell}</TableCell>
1397
- ))}
1398
- </TableRow>
1399
- ))}
1400
- </TableBody>
1401
- </Table>
1402
- </div>
1403
- )}
1404
-
1405
- {/* Document Preview */}
1406
- {msg.attachmentType === 'document' && msg.documentContent && (
1407
- <div
1408
- className="mt-3 pt-3 border-t border-border overflow-hidden"
1409
- >
1410
- <FormattedDocument
1411
- content={msg.documentContent}
1412
- maxPreviewLength={250}
1413
- />
1414
- </div>
1415
- )}
1416
-
1417
- {/* Podcast Player */}
1418
- {msg.attachmentType === 'podcast' && msg.audioUrl && (
1419
- <div
1420
- className="mt-3 pt-3 border-t border-border overflow-hidden"
1421
- >
1422
- <div className="flex items-center justify-between mb-3 min-w-0 overflow-hidden">
1423
- <div className="flex items-center gap-2 min-w-0 flex-1 overflow-hidden">
1424
- <div
1425
- className="w-6 h-6 flex items-center justify-center flex-shrink-0 rounded-[var(--radius-button)] bg-gradient-to-br from-[var(--chart-1)] via-[var(--chart-4)] to-[var(--chart-1)]"
1426
- >
1427
- <Radio className="w-3 h-3 text-white" />
1428
- </div>
1429
- <span className="text-sm font-medium break-words">
1430
- {msg.attachmentName}
1431
- </span>
1432
- </div>
1433
- <Button
1434
- variant="ghost"
1435
- size="sm"
1436
- onClick={() => msg.audioUrl && msg.attachmentName && handleDownloadPodcast(msg.audioUrl, msg.attachmentName)}
1437
- className="h-6 px-2 flex-shrink-0"
1438
- title="Baixar"
1439
- >
1440
- <Download className="w-3 h-3" />
1441
- </Button>
1442
- </div>
1443
- <div
1444
- className="rounded-[var(--radius)] p-2 overflow-hidden bg-gradient-to-r from-[var(--chart-1)]/10 to-[var(--chart-4)]/10"
1445
- >
1446
- <audio
1447
- controls
1448
- className="w-full max-w-full h-10 outline-none"
1449
- >
1450
- <source src={msg.audioUrl} type="audio/mpeg" />
1451
- Seu navegador não suporta o elemento de áudio.
1452
- </audio>
1453
- </div>
1454
- </div>
1455
- )}
1456
-
1457
- {/* Search Results */}
1458
- {msg.attachmentType === 'search' && msg.searchResults && (
1459
- <div
1460
- className="mt-3 pt-3 border-t border-border space-y-4 overflow-hidden"
1461
- >
1462
- <div className="overflow-hidden">
1463
- <div className="flex items-center gap-2 mb-3">
1464
- <Search className="w-4 h-4 text-[var(--chart-4)]" />
1465
- <h4 className="text-foreground">
1466
- Resultados Encontrados ({msg.searchResults.length})
1467
- </h4>
1468
- </div>
1469
- <div className="space-y-2 overflow-hidden">
1470
- {msg.searchResults.map((result) => (
1471
- <div
1472
- key={result.id}
1473
- onClick={() => handleOpenSearchResult(result)}
1474
- className="p-3 rounded-[var(--radius)] border border-border transition-all cursor-pointer group overflow-hidden hover:bg-muted/50"
1475
- >
1476
- <div className="flex items-start justify-between gap-2 min-w-0">
1477
- <div className="flex items-start gap-2 flex-1 min-w-0 overflow-hidden">
1478
- <div className="mt-0.5 flex-shrink-0">
1479
- {result.type === 'document' && <FileText className="w-4 h-4 text-[var(--chart-4)]" />}
1480
- {result.type === 'project' && <FolderOpen className="w-4 h-4 text-[var(--chart-1)]" />}
1481
- {result.type === 'conversation' && <MessageSquare className="w-4 h-4 text-[var(--chart-2)]" />}
1482
- {result.type === 'file' && <Folder className="w-4 h-4 text-[var(--chart-3)]" />}
1483
- {result.type === 'contact' && <Users className="w-4 h-4 text-[var(--chart-5)]" />}
1484
- </div>
1485
- <div className="flex-1 min-w-0 overflow-hidden">
1486
- <div className="flex items-start gap-2 min-w-0">
1487
- <h5 className="text-sm font-medium break-words flex-1 min-w-0 text-foreground">
1488
- {result.title}
1489
- </h5>
1490
- <span
1491
- className="px-1.5 py-0.5 rounded-sm flex-shrink-0 self-start bg-[var(--chart-4)]/10 text-[var(--chart-4)]"
1492
- >
1493
- {result.relevance}%
1494
- </span>
1495
- </div>
1496
- <p className="text-sm text-muted-foreground mt-1 line-clamp-2 break-words">
1497
- {result.description}
1498
- </p>
1499
- <div className="flex items-center gap-3 mt-2 flex-wrap min-w-0">
1500
- <span className="text-sm text-muted-foreground flex items-center gap-1 min-w-0 max-w-full">
1501
- <ExternalLink className="w-3 h-3 flex-shrink-0" />
1502
- <span className="truncate">{result.path}</span>
1503
- </span>
1504
- {result.lastModified && (
1505
- <span className="text-sm text-muted-foreground flex items-center gap-1 flex-shrink-0">
1506
- <Clock className="w-3 h-3" />
1507
- {result.lastModified}
1508
- </span>
1509
- )}
1510
- </div>
1511
- </div>
1512
- </div>
1513
- </div>
1514
- </div>
1515
- ))}
1516
- </div>
1517
- </div>
1518
-
1519
- {/* Search Sources */}
1520
- {msg.searchSources && (
1521
- <div>
1522
- <h4 className="mb-2 text-foreground">
1523
- Fontes Consultadas
1524
- </h4>
1525
- <div className="flex flex-wrap gap-2 overflow-hidden">
1526
- {msg.searchSources.map((source, index) => (
1527
- <div
1528
- key={index}
1529
- className="px-3 py-1.5 border max-w-full rounded-[var(--radius-button)] bg-muted border-border"
1530
- >
1531
- <span className="text-sm font-medium text-foreground break-words">{source.name}</span>
1532
- <span className="text-sm text-muted-foreground ml-1 whitespace-nowrap">({source.count})</span>
1533
- </div>
1534
- ))}
1535
- </div>
1536
- </div>
1537
- )}
1538
-
1539
- {/* Search Commands */}
1540
- {msg.searchCommands && msg.searchResults && (
1541
- <div>
1542
- <h4 className="mb-2 text-foreground">
1543
- O que você pode fazer com estes resultados
1544
- </h4>
1545
- <div className="grid grid-cols-1 gap-2">
1546
- {msg.searchCommands.map((command) => (
1547
- <button
1548
- key={command.id}
1549
- onClick={() => handleExecuteSearchCommand(
1550
- command.id,
1551
- msg.content.replace('🔍 Pesquisa realizada com sucesso!', '').split('"')[1] || 'pesquisa',
1552
- msg.searchResults!,
1553
- msg.id
1554
- )}
1555
- disabled={executingCommand === command.id}
1556
- className={cn(
1557
- "flex items-start gap-2 p-2 rounded-[var(--radius)] border transition-all text-left disabled:opacity-50 disabled:cursor-not-allowed",
1558
- savedSearches.includes(msg.id) && command.id === '5'
1559
- ? "border-[var(--toast-warning-border)] bg-[var(--toast-warning-bg)]/20"
1560
- : "border-border bg-transparent hover:bg-muted/50"
1561
- )}
1562
- >
1563
- {executingCommand === command.id ? (
1564
- <Loader2 className="w-5 h-5 animate-spin flex-shrink-0 text-primary" />
1565
- ) : (
1566
- <span className="flex-shrink-0">{command.icon}</span>
1567
- )}
1568
- <div className="flex-1 min-w-0 overflow-hidden">
1569
- <div className="text-sm font-medium text-foreground break-words">
1570
- {command.id === '5' && savedSearches.includes(msg.id) ? '⭐ Pesquisa salva' : command.label}
1571
- </div>
1572
- <div className="text-sm text-muted-foreground break-words">
1573
- {executingCommand === command.id ? 'Processando...' : command.description}
1574
- </div>
1575
- </div>
1576
- </button>
1577
- ))}
1578
- </div>
1579
- </div>
1580
- )}
1581
- </div>
1582
- )}
1583
- </div>
1584
-
1585
- {/* Message Actions */}
1586
- <div className="flex items-center gap-2 mt-1 px-2">
1587
- <span className="text-sm text-muted-foreground">
1588
- {msg.timestamp.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}
1589
- </span>
1590
-
1591
-
1592
- {enablePodcastGeneration && msg.type === 'assistant' && msg.attachmentType !== 'podcast' && (
1593
- <Button
1594
- variant="ghost"
1595
- size="sm"
1596
- onClick={() => handleGeneratePodcast(msg.id, msg.content)}
1597
- disabled={generatingPodcastId === msg.id}
1598
- className="h-6 w-6 p-0 disabled:opacity-50 text-muted-foreground"
1599
- title="Gerar Podcast"
1600
- aria-label="Gerar Podcast"
1601
- >
1602
- {generatingPodcastId === msg.id ? (
1603
- <Loader2 className="w-3 h-3 animate-spin" />
1604
- ) : (
1605
- <Radio className="w-3 h-3" />
1606
- )}
1607
- </Button>
1608
- )}
1609
-
1610
- {msg.type === 'assistant' && (
1611
- <div className="flex items-center gap-1 border-l border-border pl-2 ml-1">
1612
- <Button
1613
- variant="ghost"
1614
- size="icon"
1615
- className={cn(
1616
- "h-6 w-6 rounded-full hover:bg-green-100 dark:hover:bg-green-900/20 hover:text-green-600",
1617
- msg.evaluation === 'like' && "text-green-600 bg-green-100 dark:bg-green-900/20"
1618
- )}
1619
- onClick={() => handleEvaluationClick(msg.id, 'like')}
1620
- title="Gostei"
1621
- aria-label="Gostei"
1622
- >
1623
- <ThumbsUp className="h-3.5 w-3.5" />
1624
- </Button>
1625
-
1626
- <DropdownMenu>
1627
- <DropdownMenuTrigger asChild>
1628
- <Button
1629
- variant="ghost"
1630
- size="icon"
1631
- className={cn(
1632
- "h-6 w-6 rounded-full hover:bg-red-100 dark:hover:bg-red-900/20 hover:text-red-600",
1633
- msg.evaluation === 'dislike' && "text-red-600 bg-red-100 dark:bg-red-900/20"
1634
- )}
1635
- title="Não gostei"
1636
- aria-label="Não gostei"
1637
- >
1638
- <ThumbsDown className="h-3.5 w-3.5" />
1639
- </Button>
1640
- </DropdownMenuTrigger>
1641
- <DropdownMenuContent align="start">
1642
- {feedbackOptions && feedbackOptions.length > 0 ? (
1643
- feedbackOptions.map((option, idx) => (
1644
- <DropdownMenuItem key={idx} onClick={() => openFeedbackDialog(msg.id, option)}>
1645
- {option}
1646
- </DropdownMenuItem>
1647
- ))
1648
- ) : (
1649
- <>
1650
- <DropdownMenuItem onClick={() => openFeedbackDialog(msg.id, 'Não era o que eu procurava')}>
1651
- Não era o que eu procurava
1652
- </DropdownMenuItem>
1653
- <DropdownMenuItem onClick={() => openFeedbackDialog(msg.id, 'Informação incorreta')}>
1654
- Informação incorreta
1655
- </DropdownMenuItem>
1656
- <DropdownMenuItem onClick={() => openFeedbackDialog(msg.id, 'Resposta incompleta')}>
1657
- Resposta incompleta
1658
- </DropdownMenuItem>
1659
- </>
1660
- )}
1661
- <DropdownMenuItem onClick={() => openFeedbackDialog(msg.id, null)}>
1662
- Outros...
1663
- </DropdownMenuItem>
1664
- </DropdownMenuContent>
1665
- </DropdownMenu>
1666
- </div>
1667
- )}
1668
-
1669
- <Button
1670
- variant="ghost"
1671
- size="sm"
1672
- onClick={() => handleCopyMessage(msg.content, msg.id)}
1673
- className={`h-6 w-6 p-0 ${copiedId === msg.id ? 'text-[var(--toast-success-icon)]' : 'text-muted-foreground'}`}
1674
- aria-label={copiedId === msg.id ? "Copiado" : "Copiar mensagem"}
1675
- >
1676
- {copiedId === msg.id ? (
1677
- <Check className="w-3 h-3" />
1678
- ) : (
1679
- <Copy className="w-3 h-3" />
1680
- )}
1681
- </Button>
1682
- </div>
1683
- </div>
1684
- </motion.div>
525
+ msg={msg}
526
+ copiedId={copiedId}
527
+ generatingPodcastId={generatingPodcastId}
528
+ executingCommand={executingCommand}
529
+ savedSearches={savedSearches}
530
+ enablePodcastGeneration={enablePodcastGeneration}
531
+ feedbackOptions={feedbackOptions}
532
+ onCopyMessage={handleCopyMessage}
533
+ onToggleFavorite={handleToggleFavorite}
534
+ onGeneratePodcast={handleGeneratePodcast}
535
+ onDownloadDocument={handleDownloadDocument}
536
+ onDownloadPodcast={handleDownloadPodcast}
537
+ onEditDocument={handleEditDocument}
538
+ onOpenSearchResult={handleOpenSearchResult}
539
+ onExecuteSearchCommand={handleExecuteSearchCommand}
540
+ onEvaluationClick={handleEvaluationClick}
541
+ onOpenFeedbackDialog={openFeedbackDialog}
542
+ />
1685
543
  ))}
1686
544
 
1687
545
  {/* Typing Indicator */}
1688
- {isProcessing && (
1689
- <motion.div
1690
- initial={{ opacity: 0, y: 10 }}
1691
- animate={{ opacity: 1, y: 0 }}
1692
- className="flex gap-2 justify-start"
1693
- >
1694
- {/* Avatar do Assistente */}
1695
- <div className="flex-shrink-0 pt-1">
1696
- <XerticaOrbe size={32} />
1697
- </div>
1698
-
1699
- <div
1700
- className="px-4 py-3 bg-muted rounded-2xl"
1701
- >
1702
- <div className="flex gap-1">
1703
- <motion.div
1704
- className="w-2 h-2 rounded-full bg-muted-foreground"
1705
- animate={{ y: [0, -8, 0] }}
1706
- transition={{ repeat: Infinity, duration: 0.6, delay: 0 }}
1707
- />
1708
- <motion.div
1709
- className="w-2 h-2 rounded-full bg-muted-foreground"
1710
- animate={{ y: [0, -8, 0] }}
1711
- transition={{ repeat: Infinity, duration: 0.6, delay: 0.2 }}
1712
- />
1713
- <motion.div
1714
- className="w-2 h-2 rounded-full bg-muted-foreground"
1715
- animate={{ y: [0, -8, 0] }}
1716
- transition={{ repeat: Infinity, duration: 0.6, delay: 0.4 }}
1717
- />
1718
- </div>
1719
- </div>
1720
- </motion.div>
1721
- )}
546
+ {isProcessing && <AssistantTypingIndicator />}
1722
547
 
1723
548
  <div ref={messagesEndRef} />
1724
549
  </div>
@@ -1727,82 +552,21 @@ export function XerticaAssistant({
1727
552
  </div>
1728
553
  )}
1729
554
 
555
+ {/* History / Favorites Tab */}
1730
556
  {((abaSelecionada === 'historico' && showHistory) || (abaSelecionada === 'favoritos' && showFavorites)) && (
1731
- <ScrollArea className="flex-1 min-h-0">
1732
- <div className={`p-4 ${isFullPage ? 'mx-auto w-full max-w-6xl' : ''}`}>
1733
- {/* New Conversation Button */}
1734
- <Button
1735
- variant="outline"
1736
- size="sm"
1737
- onClick={handleNovaConversa}
1738
- className="w-full mb-4 justify-start"
1739
- >
1740
- <Plus className="w-4 h-4 mr-2" />
1741
- Nova Conversa
1742
- </Button>
1743
-
1744
- {/* Conversations List */}
1745
- <div className="space-y-2">
1746
- {conversasFiltradas.length === 0 ? (
1747
- <div className="text-center py-8">
1748
- <Heart className="w-12 h-12 mx-auto text-muted-foreground/50 mb-2" />
1749
- <p className="text-muted-foreground">
1750
- {abaSelecionada === 'favoritos'
1751
- ? 'Nenhuma conversa favorita ainda'
1752
- : 'Nenhuma conversa no histórico'}
1753
- </p>
1754
- </div>
1755
- ) : (
1756
- conversasFiltradas.map((conversa) => (
1757
- <div
1758
- key={conversa.id}
1759
- onClick={() => handleSelecionarConversa(conversa.id)}
1760
- className={cn(
1761
- "p-3 rounded-[var(--radius)] cursor-pointer transition-colors duration-200 border",
1762
- conversa.id === conversaAtual
1763
- ? "border-primary bg-primary/10"
1764
- : "border-border hover:bg-muted"
1765
- )}
1766
- >
1767
- <div className="flex items-start justify-between mb-1">
1768
- <h4 className="text-sm font-medium text-foreground truncate flex-1">
1769
- {conversa.title}
1770
- </h4>
1771
- <Button
1772
- variant="ghost"
1773
- size="sm"
1774
- onClick={(e) => {
1775
- e.stopPropagation();
1776
- handleToggleFavoritaConversa(conversa.id);
1777
- }}
1778
- className="h-6 w-6 p-0 flex-shrink-0 ml-1"
1779
- >
1780
- <Heart
1781
- className={cn(
1782
- "w-3 h-3",
1783
- conversa.isFavorite ? "text-destructive fill-current" : "text-muted-foreground"
1784
- )}
1785
- />
1786
- </Button>
1787
- </div>
1788
- {conversa.lastMessage && (
1789
- <p className="text-sm text-muted-foreground truncate mb-1">
1790
- {conversa.lastMessage}
1791
- </p>
1792
- )}
1793
- <p className="text-sm text-muted-foreground">
1794
- {conversa.timestamp}
1795
- </p>
1796
- </div>
1797
- ))
1798
- )}
1799
- </div>
1800
- </div>
1801
- </ScrollArea>
557
+ <AssistantConversationList
558
+ conversations={conversasFiltradas}
559
+ currentConversationId={conversaAtual}
560
+ activeTab={abaSelecionada}
561
+ isFullPage={isFullPage}
562
+ onNewConversation={handleNovaConversa}
563
+ onSelectConversation={handleSelecionarConversa}
564
+ onToggleFavorite={handleToggleFavoritaConversa}
565
+ />
1802
566
  )}
1803
567
  </div>
1804
568
 
1805
- {/* Modern Input Area - Only visible in chat mode */}
569
+ {/* Modern Input Area only visible in chat mode */}
1806
570
  {abaSelecionada === 'chat' && (
1807
571
  <ModernChatInput
1808
572
  value={mensagem}
@@ -1810,7 +574,7 @@ export function XerticaAssistant({
1810
574
  onSubmit={handleEnviarMensagem}
1811
575
  onFileUpload={enableFileAttachment ? () => fileInputRef.current?.click() : undefined}
1812
576
  onAudioUpload={enableAudioInput ? () => audioInputRef.current?.click() : undefined}
1813
- placeholder="Envie uma mensagem para Xertica"
577
+ placeholder="Envie uma mensagem"
1814
578
  disabled={isProcessing}
1815
579
  isFullPage={isFullPage}
1816
580
  enableAudioInput={enableAudioInput}
@@ -1826,38 +590,12 @@ export function XerticaAssistant({
1826
590
  </div>
1827
591
 
1828
592
  {/* Feedback Dialog */}
1829
- <Dialog open={evaluationState.isOpen} onOpenChange={(open) => !open && setEvaluationState(prev => ({ ...prev, isOpen: false }))}>
1830
- <DialogContent className="sm:max-w-[600px]">
1831
- <DialogHeader>
1832
- <DialogTitle className="pr-8">
1833
- {evaluationState.category ? `Enviar feedback: ${evaluationState.category}` : 'Enviar feedback'}
1834
- </DialogTitle>
1835
- <DialogDescription>
1836
- {evaluationState.category
1837
- ? "Gostaria de adicionar algum comentário? (Opcional)"
1838
- : "Conte-nos por que essa resposta não foi útil para que possamos melhorar."}
1839
- </DialogDescription>
1840
- </DialogHeader>
1841
- <div className="grid gap-4 py-4 px-6">
1842
- <Textarea
1843
- className="min-h-[100px]"
1844
- placeholder={evaluationState.category ? "Comentário adicional..." : "Descreva o motivo..."}
1845
- aria-label={evaluationState.category ? "Comentário adicional" : "Descreva o motivo"}
1846
- value={evaluationState.reason}
1847
- onChange={(e) => setEvaluationState(prev => ({ ...prev, reason: e.target.value }))}
1848
- rows={4}
1849
- />
1850
- </div>
1851
- <DialogFooter>
1852
- <Button variant="outline" onClick={() => setEvaluationState(prev => ({ ...prev, isOpen: false }))}>
1853
- Cancelar
1854
- </Button>
1855
- <Button onClick={handleSubmitDislike} disabled={!evaluationState.category && !evaluationState.reason.trim()}>
1856
- {evaluationState.category ? 'Confirmar e Enviar' : 'Enviar Feedback'}
1857
- </Button>
1858
- </DialogFooter>
1859
- </DialogContent>
1860
- </Dialog>
593
+ <AssistantFeedbackDialog
594
+ state={evaluationState}
595
+ onReasonChange={(reason) => setEvaluationState(prev => ({ ...prev, reason }))}
596
+ onClose={() => setEvaluationState(prev => ({ ...prev, isOpen: false }))}
597
+ onSubmit={handleSubmitDislike}
598
+ />
1861
599
  </>
1862
600
  );
1863
601
  }