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
@@ -0,0 +1,559 @@
1
+ import { useState, useEffect, useRef, useCallback } from 'react';
2
+ import { toast } from 'sonner';
3
+ import { gerarResposta } from '../../shared/assistant-utils';
4
+ import { useIsMobile } from '../../shared/use-mobile';
5
+ import type {
6
+ Message,
7
+ Conversation,
8
+ Suggestion,
9
+ AssistantTab,
10
+ AssistantMode,
11
+ SearchResult,
12
+ MockResponse,
13
+ } from './types';
14
+
15
+ // ─────────────────────────────────────────────────────────────────────────────
16
+ // Hook Props
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+
19
+ export interface UseAssistantProps {
20
+ /** Layout mode for the assistant panel */
21
+ mode?: AssistantMode;
22
+ /** Controlled expansion state */
23
+ isExpanded?: boolean;
24
+ /** Toggle callback for controlled expansion */
25
+ onToggle?: () => void;
26
+ /** Initially selected tab */
27
+ defaultTab?: AssistantTab;
28
+ /** Enables demo mode with mock AI responses */
29
+ demoMode?: boolean;
30
+ /** Custom mock responses for demo mode */
31
+ customResponses?: MockResponse[];
32
+ /** Pre-loaded messages to hydrate an existing conversation */
33
+ initialMessages?: Message[];
34
+ /** Previously saved conversations */
35
+ savedConversations?: Conversation[];
36
+ /** Suggested prompts shown in the empty state */
37
+ suggestions?: Suggestion[];
38
+ /** Callback fired when the user sends a message */
39
+ onSendMessage?: (message: string) => void;
40
+ /** Whether the assistant is currently processing a response */
41
+ isProcessing?: boolean;
42
+ /** Custom response generator — overrides demo mode defaults */
43
+ responseGenerator?: (message: string) => Promise<string | Partial<Message>> | string | Partial<Message>;
44
+ /** Extended suggestions shown when the user clicks "More suggestions" */
45
+ richSuggestions?: Suggestion[];
46
+ /** Callback fired when the user clicks a rich suggestion */
47
+ onRichAction?: (actionId: string, actionText: string) => void;
48
+ /** Callback fired when the user rates a message */
49
+ onEvaluation?: (messageId: string, type: 'like' | 'dislike', reason?: string) => void;
50
+ /** Negative feedback categories shown in the dislike dropdown */
51
+ feedbackOptions?: string[];
52
+ }
53
+
54
+ // ─────────────────────────────────────────────────────────────────────────────
55
+ // Hook Return Value
56
+ // ─────────────────────────────────────────────────────────────────────────────
57
+
58
+ export interface UseAssistantReturn {
59
+ // ── Layout state ──────────────────────────────────────────────────────────
60
+ isFullPage: boolean;
61
+ isExpanded: boolean;
62
+ isMobile: boolean;
63
+ abaSelecionada: AssistantTab;
64
+ setAbaSelecionada: (tab: AssistantTab) => void;
65
+
66
+ // ── Message state ─────────────────────────────────────────────────────────
67
+ mensagens: Message[];
68
+ setMensagens: React.Dispatch<React.SetStateAction<Message[]>>;
69
+ mensagem: string;
70
+ setMensagem: (value: string) => void;
71
+
72
+ // ── Conversation state ────────────────────────────────────────────────────
73
+ conversas: Conversation[];
74
+ conversaAtual: string | null;
75
+ conversasFiltradas: Conversation[];
76
+
77
+ // ── UI state ──────────────────────────────────────────────────────────────
78
+ copiedId: string | null;
79
+ generatingPodcastId: string | null;
80
+ executingCommand: string | null;
81
+ savedSearches: string[];
82
+ editingDocument: { content: string; title: string } | null;
83
+ setEditingDocument: (doc: { content: string; title: string } | null) => void;
84
+ showMoreSuggestions: boolean;
85
+ setShowMoreSuggestions: (show: boolean) => void;
86
+ evaluationState: {
87
+ isOpen: boolean;
88
+ messageId: string | null;
89
+ type: 'dislike' | null;
90
+ category?: string | null;
91
+ reason: string;
92
+ };
93
+ setEvaluationState: React.Dispatch<React.SetStateAction<UseAssistantReturn['evaluationState']>>;
94
+
95
+ // ── Suggestions ───────────────────────────────────────────────────────────
96
+ sugestoes: Suggestion[];
97
+
98
+ // ── Refs ──────────────────────────────────────────────────────────────────
99
+ messagesEndRef: React.RefObject<HTMLDivElement>;
100
+ fileInputRef: React.RefObject<HTMLInputElement>;
101
+ audioInputRef: React.RefObject<HTMLInputElement>;
102
+
103
+ // ── Handlers ──────────────────────────────────────────────────────────────
104
+ handleToggle: () => void;
105
+ handleExpandWithTab: (tab: AssistantTab) => void;
106
+ handleEnviarMensagem: (arg?: string | any) => Promise<void>;
107
+ handleToggleFavorite: (messageId: string) => void;
108
+ handleCopyMessage: (content: string, messageId: string) => Promise<void>;
109
+ handleGeneratePodcast: (messageId: string, content: string) => Promise<void>;
110
+ handleDownloadDocument: (content: string, fileName: string) => void;
111
+ handleDownloadPodcast: (audioUrl: string, fileName: string) => void;
112
+ handleEditDocument: (content: string, title: string) => void;
113
+ handleNovaConversa: () => void;
114
+ handleSelecionarConversa: (conversaId: string) => void;
115
+ handleToggleFavoritaConversa: (conversaId: string) => void;
116
+ handleOpenSearchResult: (result: SearchResult) => void;
117
+ handleExecuteSearchCommand: (commandId: string, searchTerm: string, results: SearchResult[], messageId: string) => Promise<void>;
118
+ handleRichSuggestionClick: (suggestion: Suggestion) => void;
119
+ handleEvaluationClick: (messageId: string, type: 'like' | 'dislike') => void;
120
+ openFeedbackDialog: (messageId: string, category: string | null) => void;
121
+ handleSubmitDislike: () => void;
122
+ }
123
+
124
+ // ─────────────────────────────────────────────────────────────────────────────
125
+ // Default suggestions
126
+ // ─────────────────────────────────────────────────────────────────────────────
127
+
128
+ const DEFAULT_SUGGESTIONS: Suggestion[] = [
129
+ { id: '1', text: 'Me ajude a criar um documento profissional' },
130
+ { id: '2', text: 'Buscar nos meus arquivos por "relatório"' },
131
+ { id: '3', text: 'Resuma as conversas importantes desta semana' },
132
+ { id: '4', text: 'Crie um podcast sobre o último projeto' },
133
+ ];
134
+
135
+ // ─────────────────────────────────────────────────────────────────────────────
136
+ // Hook
137
+ // ─────────────────────────────────────────────────────────────────────────────
138
+
139
+ /**
140
+ * `useAssistant` — Headless hook for the XerticaAssistant component.
141
+ *
142
+ * Encapsulates all state management and business logic for the AI assistant panel.
143
+ * Use this hook when you need to build a fully custom assistant UI while reusing
144
+ * the same state logic as the default `XerticaAssistant` component.
145
+ *
146
+ * @example
147
+ * ```tsx
148
+ * import { useAssistant } from 'xertica-ui/assistant';
149
+ *
150
+ * function MyCustomAssistant() {
151
+ * const {
152
+ * mensagens,
153
+ * mensagem,
154
+ * setMensagem,
155
+ * handleEnviarMensagem,
156
+ * isExpanded,
157
+ * handleToggle,
158
+ * } = useAssistant({ demoMode: true });
159
+ *
160
+ * return (
161
+ * <div>
162
+ * {mensagens.map(msg => <p key={msg.id}>{msg.content}</p>)}
163
+ * <input value={mensagem} onChange={e => setMensagem(e.target.value)} />
164
+ * <button onClick={() => handleEnviarMensagem()}>Send</button>
165
+ * </div>
166
+ * );
167
+ * }
168
+ * ```
169
+ */
170
+ export function useAssistant({
171
+ mode = 'expanded',
172
+ isExpanded: controlledIsExpanded,
173
+ onToggle,
174
+ defaultTab = 'chat',
175
+ demoMode = true,
176
+ customResponses = [],
177
+ initialMessages = [],
178
+ savedConversations = [],
179
+ suggestions: propSuggestions,
180
+ onSendMessage,
181
+ isProcessing = false,
182
+ responseGenerator,
183
+ richSuggestions = [],
184
+ onRichAction,
185
+ onEvaluation,
186
+ }: UseAssistantProps = {}): UseAssistantReturn {
187
+
188
+ // ── Layout state ────────────────────────────────────────────────────────────
189
+ const isFullPage = mode === 'fullPage';
190
+ const [internalIsExpanded, setInternalIsExpanded] = useState(controlledIsExpanded ?? true);
191
+ const isExpanded = controlledIsExpanded ?? internalIsExpanded;
192
+ const isMobile = useIsMobile();
193
+
194
+ // ── Tab state ───────────────────────────────────────────────────────────────
195
+ const [abaSelecionada, setAbaSelecionada] = useState<AssistantTab>(defaultTab);
196
+
197
+ // ── Message state ───────────────────────────────────────────────────────────
198
+ const [mensagens, setMensagens] = useState<Message[]>(initialMessages);
199
+ const [mensagem, setMensagem] = useState('');
200
+
201
+ // ── Conversation state ──────────────────────────────────────────────────────
202
+ const [conversas, setConversas] = useState<Conversation[]>(savedConversations);
203
+ const [conversaAtual, setConversaAtual] = useState<string | null>(null);
204
+
205
+ // ── UI state ────────────────────────────────────────────────────────────────
206
+ const [copiedId, setCopiedId] = useState<string | null>(null);
207
+ const [generatingPodcastId, setGeneratingPodcastId] = useState<string | null>(null);
208
+ const [executingCommand, setExecutingCommand] = useState<string | null>(null);
209
+ const [savedSearches, setSavedSearches] = useState<string[]>([]);
210
+ const [editingDocument, setEditingDocument] = useState<{ content: string; title: string } | null>(null);
211
+ const [showMoreSuggestions, setShowMoreSuggestions] = useState(false);
212
+ const [evaluationState, setEvaluationState] = useState<UseAssistantReturn['evaluationState']>({
213
+ isOpen: false,
214
+ messageId: null,
215
+ type: null,
216
+ category: null,
217
+ reason: '',
218
+ });
219
+
220
+ // ── Refs ────────────────────────────────────────────────────────────────────
221
+ const messagesEndRef = useRef<HTMLDivElement>(null);
222
+ const fileInputRef = useRef<HTMLInputElement>(null);
223
+ const audioInputRef = useRef<HTMLInputElement>(null);
224
+
225
+ // ── Suggestions ─────────────────────────────────────────────────────────────
226
+ const sugestoes = propSuggestions ?? DEFAULT_SUGGESTIONS;
227
+
228
+ // ── Effects ─────────────────────────────────────────────────────────────────
229
+
230
+ // Auto-scroll on new messages
231
+ useEffect(() => {
232
+ if (messagesEndRef.current && abaSelecionada === 'chat') {
233
+ messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
234
+ }
235
+ }, [mensagens, abaSelecionada]);
236
+
237
+ // Sync initial messages when they change
238
+ useEffect(() => {
239
+ if (initialMessages && initialMessages.length > 0) {
240
+ setMensagens(initialMessages);
241
+ }
242
+ }, [initialMessages]);
243
+
244
+ // ── Computed ────────────────────────────────────────────────────────────────
245
+
246
+ const conversasFiltradas = conversas.filter(conversa => {
247
+ if (abaSelecionada === 'favoritos') return conversa.isFavorite;
248
+ return true;
249
+ });
250
+
251
+ // ── Handlers ────────────────────────────────────────────────────────────────
252
+
253
+ const handleToggle = useCallback(() => {
254
+ if (onToggle) {
255
+ onToggle();
256
+ } else {
257
+ setInternalIsExpanded(prev => !prev);
258
+ }
259
+ }, [onToggle]);
260
+
261
+ const handleExpandWithTab = useCallback((tab: AssistantTab) => {
262
+ setAbaSelecionada(tab);
263
+ if (onToggle) {
264
+ onToggle();
265
+ } else {
266
+ setInternalIsExpanded(true);
267
+ }
268
+ }, [onToggle]);
269
+
270
+ const handleEnviarMensagem = useCallback(async (arg?: string | any) => {
271
+ let msgToSend = mensagem;
272
+
273
+ if (typeof arg === 'string' && !['document', 'podcast', 'search'].includes(arg)) {
274
+ msgToSend = arg;
275
+ }
276
+
277
+ if (!msgToSend.trim() || isProcessing) return;
278
+
279
+ const novaMensagem: Message = {
280
+ id: `msg-${Date.now()}`,
281
+ type: 'user',
282
+ content: msgToSend,
283
+ timestamp: new Date(),
284
+ isFavorite: false,
285
+ };
286
+
287
+ setMensagens(prev => [...prev, novaMensagem]);
288
+
289
+ if (onSendMessage) {
290
+ onSendMessage(msgToSend);
291
+ }
292
+
293
+ if (demoMode || responseGenerator) {
294
+ const mensagemAtual = msgToSend;
295
+ setTimeout(async () => {
296
+ let resposta: string | Partial<Message>;
297
+
298
+ if (responseGenerator) {
299
+ resposta = await responseGenerator(mensagemAtual);
300
+ } else {
301
+ resposta = gerarResposta(mensagemAtual, customResponses);
302
+ }
303
+
304
+ let novaMensagemIA: Message = {
305
+ id: `msg-${Date.now()}-ia`,
306
+ type: 'assistant',
307
+ content: '',
308
+ timestamp: new Date(),
309
+ isFavorite: false,
310
+ };
311
+
312
+ if (typeof resposta === 'string') {
313
+ novaMensagemIA.content = resposta;
314
+ } else {
315
+ novaMensagemIA = { ...novaMensagemIA, ...resposta };
316
+ }
317
+
318
+ setMensagens(prev => [...prev, novaMensagemIA]);
319
+ }, 1000 + Math.random() * 1000);
320
+ }
321
+
322
+ setMensagem('');
323
+ }, [mensagem, isProcessing, onSendMessage, demoMode, responseGenerator, customResponses]);
324
+
325
+ const handleToggleFavorite = useCallback((messageId: string) => {
326
+ setMensagens(prev =>
327
+ prev.map(msg =>
328
+ msg.id === messageId ? { ...msg, isFavorite: !msg.isFavorite } : msg
329
+ )
330
+ );
331
+ }, []);
332
+
333
+ const handleCopyMessage = useCallback(async (content: string, messageId: string) => {
334
+ try {
335
+ if (navigator.clipboard && navigator.clipboard.writeText) {
336
+ try {
337
+ await navigator.clipboard.writeText(content);
338
+ setCopiedId(messageId);
339
+ setTimeout(() => setCopiedId(null), 2000);
340
+ } catch (clipboardError: any) {
341
+ if (clipboardError.name === 'NotAllowedError' || clipboardError.message.includes('permissions policy')) {
342
+ throw new Error('Clipboard permission denied, falling back');
343
+ }
344
+ throw clipboardError;
345
+ }
346
+ } else {
347
+ throw new Error('Clipboard API not available');
348
+ }
349
+ } catch {
350
+ try {
351
+ const textArea = document.createElement('textarea');
352
+ textArea.value = content;
353
+ textArea.style.position = 'fixed';
354
+ textArea.style.left = '-999999px';
355
+ textArea.style.top = '-999999px';
356
+ document.body.appendChild(textArea);
357
+ textArea.focus();
358
+ textArea.select();
359
+ const successful = document.execCommand('copy');
360
+ document.body.removeChild(textArea);
361
+ if (successful) {
362
+ setCopiedId(messageId);
363
+ setTimeout(() => setCopiedId(null), 2000);
364
+ }
365
+ } catch (fallbackErr) {
366
+ console.error('Falha ao copiar:', fallbackErr);
367
+ }
368
+ }
369
+ }, []);
370
+
371
+ const handleGeneratePodcast = useCallback(async (messageId: string, content: string) => {
372
+ setGeneratingPodcastId(messageId);
373
+ setTimeout(() => {
374
+ setMensagens(prev =>
375
+ prev.map(msg =>
376
+ msg.id === messageId
377
+ ? {
378
+ ...msg,
379
+ attachmentType: 'podcast' as const,
380
+ attachmentName: 'Podcast - ' + content.substring(0, 30) + '...',
381
+ audioUrl: 'data:audio/mpeg;base64,//uQx...',
382
+ }
383
+ : msg
384
+ )
385
+ );
386
+ setGeneratingPodcastId(null);
387
+ }, 2000);
388
+ }, []);
389
+
390
+ const handleDownloadDocument = useCallback((content: string, fileName: string) => {
391
+ const blob = new Blob([content], { type: 'text/markdown' });
392
+ const url = URL.createObjectURL(blob);
393
+ const a = document.createElement('a');
394
+ a.href = url;
395
+ a.download = fileName.endsWith('.md') ? fileName : fileName + '.md';
396
+ document.body.appendChild(a);
397
+ a.click();
398
+ document.body.removeChild(a);
399
+ URL.revokeObjectURL(url);
400
+ }, []);
401
+
402
+ const handleDownloadPodcast = useCallback((audioUrl: string, fileName: string) => {
403
+ const a = document.createElement('a');
404
+ a.href = audioUrl;
405
+ a.download = fileName.endsWith('.mp3') ? fileName : fileName + '.mp3';
406
+ document.body.appendChild(a);
407
+ a.click();
408
+ document.body.removeChild(a);
409
+ }, []);
410
+
411
+ const handleEditDocument = useCallback((content: string, title: string) => {
412
+ setEditingDocument({ content, title });
413
+ }, []);
414
+
415
+ const handleNovaConversa = useCallback(() => {
416
+ setMensagens([]);
417
+ setConversaAtual(null);
418
+ setAbaSelecionada('chat');
419
+ }, []);
420
+
421
+ const handleSelecionarConversa = useCallback((conversaId: string) => {
422
+ const conversa = conversas.find(c => c.id === conversaId);
423
+ if (conversa) {
424
+ setMensagens(conversa.messages);
425
+ setConversaAtual(conversaId);
426
+ setAbaSelecionada('chat');
427
+ }
428
+ }, [conversas]);
429
+
430
+ const handleToggleFavoritaConversa = useCallback((conversaId: string) => {
431
+ setConversas(prev =>
432
+ prev.map(conv =>
433
+ conv.id === conversaId ? { ...conv, isFavorite: !conv.isFavorite } : conv
434
+ )
435
+ );
436
+ }, []);
437
+
438
+ const handleOpenSearchResult = useCallback((result: SearchResult) => {
439
+ console.log('Abrir resultado:', result);
440
+ }, []);
441
+
442
+ const handleExecuteSearchCommand = useCallback(async (
443
+ commandId: string,
444
+ searchTerm: string,
445
+ results: SearchResult[],
446
+ messageId: string
447
+ ) => {
448
+ setExecutingCommand(commandId);
449
+ setTimeout(() => {
450
+ if (commandId === '5') {
451
+ setSavedSearches(prev => [...prev, messageId]);
452
+ }
453
+ setExecutingCommand(null);
454
+ }, 1500);
455
+ }, []);
456
+
457
+ const handleRichSuggestionClick = useCallback((suggestion: Suggestion) => {
458
+ if (onRichAction) {
459
+ onRichAction(suggestion.id, suggestion.text);
460
+ } else {
461
+ handleEnviarMensagem(suggestion.text);
462
+ }
463
+ setShowMoreSuggestions(false);
464
+ }, [onRichAction, handleEnviarMensagem]);
465
+
466
+ const handleEvaluationClick = useCallback((messageId: string, type: 'like' | 'dislike') => {
467
+ if (type === 'like') {
468
+ if (onEvaluation) onEvaluation(messageId, 'like');
469
+ setMensagens(prev => prev.map(m => m.id === messageId ? { ...m, evaluation: 'like' as const } : m));
470
+ toast.success("Obrigado por avaliar positivamente!");
471
+ }
472
+ }, [onEvaluation]);
473
+
474
+ const openFeedbackDialog = useCallback((messageId: string, category: string | null) => {
475
+ setEvaluationState({
476
+ isOpen: true,
477
+ messageId,
478
+ type: 'dislike',
479
+ category,
480
+ reason: '',
481
+ });
482
+ }, []);
483
+
484
+ const handleSubmitDislike = useCallback(() => {
485
+ if (evaluationState.messageId) {
486
+ const finalReason = evaluationState.category
487
+ ? (evaluationState.reason ? `${evaluationState.category}: ${evaluationState.reason}` : evaluationState.category)
488
+ : evaluationState.reason;
489
+
490
+ if (onEvaluation) {
491
+ onEvaluation(evaluationState.messageId, 'dislike', finalReason);
492
+ }
493
+
494
+ setMensagens(prev =>
495
+ prev.map(m =>
496
+ m.id === evaluationState.messageId
497
+ ? { ...m, evaluation: 'dislike' as const, evaluationReason: finalReason }
498
+ : m
499
+ )
500
+ );
501
+ toast.success("Obrigado pelo seu feedback. Vamos melhorar.");
502
+ }
503
+ setEvaluationState({ isOpen: false, messageId: null, type: null, category: null, reason: '' });
504
+ }, [evaluationState, onEvaluation]);
505
+
506
+ return {
507
+ // Layout
508
+ isFullPage,
509
+ isExpanded,
510
+ isMobile,
511
+ abaSelecionada,
512
+ setAbaSelecionada,
513
+ // Messages
514
+ mensagens,
515
+ setMensagens,
516
+ mensagem,
517
+ setMensagem,
518
+ // Conversations
519
+ conversas,
520
+ conversaAtual,
521
+ conversasFiltradas,
522
+ // UI
523
+ copiedId,
524
+ generatingPodcastId,
525
+ executingCommand,
526
+ savedSearches,
527
+ editingDocument,
528
+ setEditingDocument,
529
+ showMoreSuggestions,
530
+ setShowMoreSuggestions,
531
+ evaluationState,
532
+ setEvaluationState,
533
+ // Suggestions
534
+ sugestoes,
535
+ // Refs
536
+ messagesEndRef,
537
+ fileInputRef,
538
+ audioInputRef,
539
+ // Handlers
540
+ handleToggle,
541
+ handleExpandWithTab,
542
+ handleEnviarMensagem,
543
+ handleToggleFavorite,
544
+ handleCopyMessage,
545
+ handleGeneratePodcast,
546
+ handleDownloadDocument,
547
+ handleDownloadPodcast,
548
+ handleEditDocument,
549
+ handleNovaConversa,
550
+ handleSelecionarConversa,
551
+ handleToggleFavoritaConversa,
552
+ handleOpenSearchResult,
553
+ handleExecuteSearchCommand,
554
+ handleRichSuggestionClick,
555
+ handleEvaluationClick,
556
+ openFeedbackDialog,
557
+ handleSubmitDislike,
558
+ };
559
+ }