xertica-ui 2.1.3 → 2.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/README.md +1 -1
- package/bin/cli.ts +1 -1
- package/components/assistant/xertica-assistant/hooks/index.ts +8 -0
- package/components/assistant/xertica-assistant/hooks/use-assistant-conversations.ts +119 -0
- package/components/assistant/xertica-assistant/hooks/use-assistant-messages.ts +323 -0
- package/components/assistant/xertica-assistant/hooks/use-assistant-suggestions.ts +64 -0
- package/components/assistant/xertica-assistant/parts/AssistantCollapsedView.tsx +97 -0
- package/components/assistant/xertica-assistant/parts/AssistantConversationList.tsx +104 -0
- package/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.tsx +81 -0
- package/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.tsx +86 -0
- package/components/assistant/xertica-assistant/parts/AssistantHeader.tsx +77 -0
- package/components/assistant/xertica-assistant/parts/AssistantMessageBubble.tsx +573 -0
- package/components/assistant/xertica-assistant/parts/AssistantTabBar.tsx +65 -0
- package/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.tsx +41 -0
- package/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.tsx +98 -0
- package/components/assistant/xertica-assistant/parts/index.ts +16 -0
- package/components/assistant/xertica-assistant/types.ts +139 -0
- package/components/assistant/xertica-assistant/use-assistant.ts +294 -564
- package/components/assistant/xertica-assistant/xertica-assistant.tsx +130 -1109
- package/components/blocks/card-patterns/card-patterns.test.tsx +448 -0
- package/components/brand/theme-toggle/ThemeToggle.tsx +8 -27
- package/components/hooks/index.ts +22 -7
- package/components/hooks/use-layout-shortcuts.ts +46 -0
- package/components/layout/sidebar/sidebar.tsx +26 -41
- package/components/layout/sidebar/use-sidebar.ts +2 -11
- package/components/media/audio-player/AudioPlayer.tsx +140 -215
- package/components/media/audio-player/use-audio-player.ts +308 -0
- package/components/shared/CustomTooltipContent.tsx +52 -0
- package/components/ui/chart/chart.test.tsx +236 -65
- package/components/ui/chart/chart.tsx +59 -2347
- package/components/ui/chart/parts/chart-dashboard.tsx +1025 -0
- package/components/ui/chart/parts/chart-metric.tsx +622 -0
- package/components/ui/chart/parts/chart-primitives.tsx +432 -0
- package/components/ui/chart/parts/chart-shared.tsx +201 -0
- package/components/ui/chart/parts/chart-utils.ts +145 -0
- package/components/ui/chart/parts/index.ts +5 -0
- package/components/ui/file-upload/file-upload.test.tsx +6 -2
- package/components/ui/file-upload/file-upload.tsx +17 -5
- package/components/ui/file-upload/use-file-upload.test.ts +173 -0
- package/components/ui/file-upload/use-file-upload.ts +76 -25
- package/components/ui/index.ts +1 -1
- package/components/ui/pagination/pagination.tsx +17 -0
- package/components/ui/pagination/use-pagination.test.ts +104 -0
- package/components/ui/pagination/use-pagination.ts +33 -16
- package/components/ui/rich-text-editor/rich-text-editor.tsx +9 -3
- package/components/ui/rich-text-editor/use-rich-text-editor.test.ts +110 -0
- package/components/ui/rich-text-editor/use-rich-text-editor.ts +42 -15
- package/components/ui/stepper/stepper.tsx +21 -2
- package/components/ui/stepper/use-stepper.test.ts +109 -0
- package/components/ui/stepper/use-stepper.ts +4 -1
- package/components/ui/tree-view/tree-view.tsx +29 -4
- package/components/ui/tree-view/use-tree-view.test.ts +91 -0
- package/components/ui/tree-view/use-tree-view.ts +38 -19
- package/contexts/AssistenteContext.tsx +17 -54
- package/contexts/BrandColorsContext.tsx +32 -43
- package/contexts/LayoutContext.tsx +24 -50
- package/dist/AssistantChart-DIpshm3i.js +4784 -0
- package/dist/AssistantChart-zjsy2GaZ.cjs +4810 -0
- package/dist/AudioPlayer-CGRUtUdN.js +937 -0
- package/dist/AudioPlayer-IAU5q5T1.cjs +936 -0
- package/dist/CustomTooltipContent-DHjkY0ww.js +40 -0
- package/dist/CustomTooltipContent-c_K-DWRr.cjs +56 -0
- package/dist/FeatureCard-CxC-7C-C.cjs +300 -0
- package/dist/FeatureCard-DbHWCb4E.js +301 -0
- package/dist/LanguageContext-B_KFTCzT.cjs +656 -0
- package/dist/LanguageContext-CS14yCpi.js +657 -0
- package/dist/LayoutContext-BDmcZfMH.cjs +84 -0
- package/dist/LayoutContext-dbQvdC4O.js +85 -0
- package/dist/ThemeContext-RTy1m2Uq.js +82 -0
- package/dist/ThemeContext-bSzuOit2.cjs +81 -0
- package/dist/VerifyEmailPage-CqKsR2v8.js +2827 -0
- package/dist/VerifyEmailPage-s-1X3LDJ.cjs +2826 -0
- package/dist/XerticaProvider-B7EVH-NF.js +40 -0
- package/dist/XerticaProvider-ET0ihewn.cjs +39 -0
- package/dist/XerticaXLogo-D8jf0SNv.cjs +214 -0
- package/dist/XerticaXLogo-fAJMy3H4.js +215 -0
- package/dist/assistant.cjs.js +1 -1
- package/dist/assistant.es.js +1 -1
- package/dist/blocks.cjs.js +1 -1
- package/dist/blocks.es.js +1 -1
- package/dist/brand.cjs.js +2 -2
- package/dist/brand.es.js +2 -2
- package/dist/cli.js +1 -1
- package/dist/components/assistant/xertica-assistant/hooks/index.d.ts +6 -0
- package/dist/components/assistant/xertica-assistant/hooks/use-assistant-conversations.d.ts +21 -0
- package/dist/components/assistant/xertica-assistant/hooks/use-assistant-messages.d.ts +49 -0
- package/dist/components/assistant/xertica-assistant/hooks/use-assistant-suggestions.d.ts +16 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantCollapsedView.d.ts +13 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantConversationList.d.ts +16 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.d.ts +17 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.d.ts +19 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantHeader.d.ts +11 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantMessageBubble.d.ts +29 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantTabBar.d.ts +13 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.d.ts +4 -0
- package/dist/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.d.ts +17 -0
- package/dist/components/assistant/xertica-assistant/parts/index.d.ts +16 -0
- package/dist/components/assistant/xertica-assistant/types.d.ts +106 -0
- package/dist/components/assistant/xertica-assistant/use-assistant.d.ts +1 -2
- package/dist/components/assistant/xertica-assistant/xertica-assistant.d.ts +7 -109
- package/dist/components/hooks/index.d.ts +13 -0
- package/dist/components/hooks/use-layout-shortcuts.d.ts +22 -0
- package/dist/components/media/audio-player/AudioPlayer.d.ts +4 -1
- package/dist/components/media/audio-player/use-audio-player.d.ts +72 -0
- package/dist/components/shared/CustomTooltipContent.d.ts +20 -0
- package/dist/components/ui/alert/alert.d.ts +1 -1
- package/dist/components/ui/badge/badge.d.ts +1 -1
- package/dist/components/ui/button/button.d.ts +2 -2
- package/dist/components/ui/chart/chart.d.ts +9 -324
- package/dist/components/ui/chart/parts/chart-dashboard.d.ts +113 -0
- package/dist/components/ui/chart/parts/chart-metric.d.ts +118 -0
- package/dist/components/ui/chart/parts/chart-primitives.d.ts +101 -0
- package/dist/components/ui/chart/parts/chart-shared.d.ts +20 -0
- package/dist/components/ui/chart/parts/chart-utils.d.ts +12 -0
- package/dist/components/ui/chart/parts/index.d.ts +5 -0
- package/dist/components/ui/file-upload/use-file-upload.d.ts +9 -15
- package/dist/components/ui/index.d.ts +1 -1
- package/dist/components/ui/pagination/pagination.d.ts +5 -3
- package/dist/components/ui/tree-view/tree-view.d.ts +2 -0
- package/dist/contexts/AssistenteContext.d.ts +10 -49
- package/dist/hooks.cjs.js +37 -10
- package/dist/hooks.es.js +32 -4
- package/dist/index.cjs.js +54 -40
- package/dist/index.es.js +259 -245
- package/dist/layout.cjs.js +3 -10
- package/dist/layout.es.js +3 -10
- package/dist/media.cjs.js +1 -1
- package/dist/media.es.js +1 -1
- package/dist/pages.cjs.js +1 -1
- package/dist/pages.es.js +1 -1
- package/dist/progress-DPtzoVV8.js +175 -0
- package/dist/progress-EeaoqqUs.cjs +191 -0
- package/dist/rich-text-editor-CMgSN_w2.js +1189 -0
- package/dist/rich-text-editor-lyYE2ZG5.cjs +1207 -0
- package/dist/sidebar-BViy8Eeu.js +765 -0
- package/dist/sidebar-BxGXsDAd.cjs +764 -0
- package/dist/ui.cjs.js +46 -32
- package/dist/ui.es.js +237 -223
- package/dist/use-audio-player-NKsWyjWu.cjs +180 -0
- package/dist/use-audio-player-nv8ZSGa1.js +181 -0
- package/dist/use-file-upload-BcjEo2S5.js +404 -0
- package/dist/use-file-upload-CRJR68Tj.cjs +403 -0
- package/dist/use-rich-text-editor-DjiddBGv.js +282 -0
- package/dist/use-rich-text-editor-lpeswbCs.cjs +281 -0
- package/dist/utils/color-utils.d.ts +51 -0
- package/dist/xertica-assistant-BdiZag0h.js +2187 -0
- package/dist/xertica-assistant-DUBpmEgo.cjs +2186 -0
- package/dist/xertica-ui.css +1 -1
- package/docs/architecture-improvements.md +463 -0
- package/docs/architecture.md +1 -1
- package/docs/components/assistant-chart.md +1 -1
- package/docs/components/audio-player.md +46 -0
- package/docs/components/branding.md +251 -0
- package/docs/components/chart.md +11 -0
- package/docs/components/code-block.md +108 -0
- package/docs/components/file-upload.md +15 -9
- package/docs/components/formatted-document.md +113 -0
- package/docs/components/hooks.md +909 -0
- package/docs/components/image-with-fallback.md +106 -0
- package/docs/components/map-layers.md +140 -0
- package/docs/components/modern-chat-input.md +163 -0
- package/docs/components/pages.md +351 -0
- package/docs/components/pagination.md +16 -5
- package/docs/components/rich-text-editor.md +2 -1
- package/docs/components/sidebar.md +43 -11
- package/docs/components/stepper.md +1 -0
- package/docs/components/tree-view.md +3 -2
- package/docs/doc-audit.md +223 -0
- package/docs/getting-started.md +154 -0
- package/docs/llms.md +25 -5
- package/docs/patterns/detail-page.md +276 -0
- package/docs/patterns/settings.md +346 -0
- package/docs/patterns/wizard.md +217 -0
- package/llms-compact.txt +50 -1
- package/llms-full.txt +8 -3
- package/package.json +1 -1
- package/templates/CLAUDE.md +57 -8
- package/templates/README.md +152 -0
- package/templates/guidelines/Guidelines.md +307 -72
- package/templates/package.json +5 -2
- package/templates/src/app/App.tsx +6 -1
- package/templates/src/app/components/AppLayout.tsx +7 -5
- package/templates/src/app/components/AuthGuard.tsx +53 -30
- package/templates/src/features/auth/ui/ForgotPasswordContent.tsx +0 -2
- package/templates/src/features/auth/ui/LoginContent.tsx +4 -3
- package/templates/src/features/auth/ui/ResetPasswordContent.tsx +11 -10
- package/templates/src/features/auth/ui/VerifyEmailContent.tsx +4 -6
- package/templates/src/features/home/ui/HomeContent.tsx +64 -50
- package/templates/src/features/settings/index.ts +1 -0
- package/templates/src/features/settings/ui/SettingsContent.tsx +214 -0
- package/templates/src/features/template/ui/CrudTemplate.tsx +266 -104
- package/templates/src/features/template/ui/DashboardTemplate.tsx +187 -99
- package/templates/src/features/template/ui/FormTemplate.tsx +190 -83
- package/templates/src/features/template/ui/LoginTemplate.tsx +7 -9
- package/templates/src/pages/AssistantPage.tsx +10 -5
- package/templates/src/pages/HomePage.tsx +5 -15
- package/templates/src/pages/NotFoundPage.tsx +29 -0
- package/templates/src/pages/SettingsPage.tsx +21 -0
- package/templates/src/shared/config/assistant.ts +20 -0
- package/templates/src/shared/config/navigation.ts +6 -5
- package/templates/src/shared/lib/api.ts +79 -0
- package/templates/src/shared/lib/auth.ts +11 -0
- package/templates/src/shared/types/auth.ts +6 -0
- package/templates/src/shared/ui/PageContent.tsx +35 -0
- package/templates/src/styles/index.css +2 -2
- package/templates/src/vite-env.d.ts +12 -0
- package/utils/color-utils.ts +72 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,63 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [2.1.5] — 2026-05-19
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`use-rich-text-editor` — `wordCount`/`characterCount` não reativos** — os contadores eram lidos via `editorRef.current` no corpo do render (sem estado reativo), ficando stale após cada keystroke. Convertidos para `useState` e atualizados dentro de `handleInput`. Um segundo `useEffect([value])` foi adicionado para atualizar os contadores quando o valor externo é sincronizado ao editor.
|
|
15
|
+
- **`use-assistant-messages` — `setTimeout` da resposta AI nunca cancelado** — o timer da resposta simulada (1–2 s) não era cancelado ao desmontar o componente, causando chamadas de `setMensagens` em componente desmontado. Adicionado `responseTimerRef` + `useEffect` cleanup com `clearTimeout`.
|
|
16
|
+
- **`use-assistant-messages` — `initialMessages` resetava a conversa a cada re-render** — o `useEffect` que sincronizava `initialMessages` disparava toda vez que a referência do array mudava, sobrescrevendo mensagens digitadas pelo usuário. Corrigido com `hydratedRef` (hydration one-shot).
|
|
17
|
+
- **`use-assistant-messages` — auto-scroll forçado ignorava scroll do usuário** — `scrollIntoView` era chamado incondicionalmente em todo novo `mensagens`. Agora só rola se o usuário já está a menos de 120 px do final do container.
|
|
18
|
+
- **`use-assistant-conversations` — `conversas` não sincronizado quando `savedConversations` muda** — o estado era inicializado uma vez no mount; atualizações externas da prop eram ignoradas. Adicionado `useEffect([savedConversations])` para re-sincronizar.
|
|
19
|
+
- **`use-assistant-conversations` — `setTimeout` do comando não limpo no unmount** — `commandTimerRef` + cleanup `useEffect` adicionados para cancelar o timer ao desmontar.
|
|
20
|
+
- **`use-audio-player` — `eslint-disable` mascarando deps stale no modo-switch effect** — o efeito de restauração de estado ao trocar `isFloating`/`variant` lia `currentTime` e `isPlaying` de uma closure stale. Corrigido com `currentTimeRef` e `isPlayingRef` (padrão "latest ref"), eliminando o `eslint-disable`.
|
|
21
|
+
- **`use-rich-text-editor` — `eslint-disable` no `useEffect` de seleção** — o listener de `selectionchange` chamava `updateActiveFormats` via closure stale. Separado em dois effects: init (mount-only) e listener (via `updateActiveFormatsRef` para evitar re-registro a cada render).
|
|
22
|
+
- **`use-assistant-conversations` — `conversasFiltradas` não memoizado** — recalculado em todo re-render do componente pai. Substituído por `useMemo([conversas, abaSelecionada])`.
|
|
23
|
+
- **`use-assistant-messages` — `handleEnviarMensagem` tipado como `string | any`** — tipo colapsado para `any`. Corrigido para `(arg?: string) => Promise<void>`.
|
|
24
|
+
- **`FileUpload` / `useFileUpload` — drop-zone inacessível por teclado** — adicionados `role="button"`, `tabIndex`, `onKeyDown` (Enter/Space abre o file picker) e `focus-visible:ring` à drop-zone.
|
|
25
|
+
- **`useFileUpload` — `accept` não validado em drag-and-drop** — o browser só aplica `accept` nativamente no picker; arquivos de tipo diferente podiam ser soltos livremente. Adicionado helper `matchesAccept` que filtra por MIME type e extensão antes da validação de tamanho/contagem.
|
|
26
|
+
- **`FileUpload` — tamanho de arquivo sempre em KB** — arquivos grandes exibiam `102400.00 KB`. Corrigido para exibição adaptativa KB/MB.
|
|
27
|
+
- **`useStepper` — `initialStep` não clampado no mount** — valores fora de `[1, totalSteps]` vazavam diretamente para o estado. Corrigido com lazy initializer `Math.min(Math.max(1, initialStep), totalSteps)`.
|
|
28
|
+
- **`use-pagination` — páginas duplicadas quando `siblingCount` grande** — o algoritmo de construção do `items` array podia inserir a mesma página duas vezes quando o sibling range coincidia com os push explícitos de `page 2` / `page totalPages-1`. Reescrito com `Set` deduplicado; ellipsis inferido do gap entre páginas consecutivas.
|
|
29
|
+
- **`use-tree-view` — Space key disparava `handleSelect` em nós pai** — segundo a spec WAI-ARIA tree, Space em branch deve apenas expandir/colapsar; Select fica reservado para Enter e nós folha. Corrigido no `handleKeyDown`.
|
|
30
|
+
- **`chart-metric.tsx` — `stateClassName` passado duas vezes para `getChartState`** — em todas as 3 instâncias de `RadarMetricChart`, `PieMetricChart` e `RadialBarMetricChart`, `stateClassName` aparecia dentro do props object e novamente como 3º argumento separado. 3º argumento removido.
|
|
31
|
+
- **`use-assistant-conversations` — magic string `'5'` como command ID hardcoded** — comentário de TODO mantido; o timer cleanup foi aplicado independentemente.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **`useFileUpload` — `clearFiles()`** — nova função na API pública do hook que remove todos os arquivos aceitos e limpa o `errorMessage` em uma única chamada.
|
|
36
|
+
- **`useFileUpload` — prop `accept`** — o hook agora aceita e aplica o filtro `accept` tanto no drag-and-drop quanto no click-to-browse (anteriormente, `accept` era apenas passado ao `<input>` sem enforçar em drops).
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **`Stepper` — ARIA** — adicionados `role="list"` + `aria-label="Progresso: etapa X de Y"` no `<Stepper>` e `role="listitem"` + `aria-current="step"` + `aria-label` descritivo em cada `<Step>`, tornando o componente acessível a screen readers.
|
|
41
|
+
- **`PaginationLink` — `disabled` prop + `href` fallback** — `PaginationLink` agora aceita `disabled` (aplica `aria-disabled`, `tabIndex=-1` e `pointer-events-none`) e faz fallback para `href="#"` quando `href` não é passado, garantindo que o link permaneça focusável via Tab em SPAs. `PaginationPrevious` e `PaginationNext` receberam o `disabled` pass-through.
|
|
42
|
+
- **`TreeView` — ARIA e roving tabindex** — adicionado `aria-label` (padrão `"Navegação em árvore"`) no `role="tree"` (WCAG 4.1.2); implementado roving tabindex correto: `focusableId = selectedId ?? firstRootId` garante que sempre haja um nó focusável por Tab mesmo sem seleção ativa.
|
|
43
|
+
- **`RichTextEditor` — ARIA no `contentEditable`** — adicionados `role="textbox"`, `aria-multiline="true"`, `aria-label`, `aria-readonly` e `aria-disabled` ao div editável.
|
|
44
|
+
- **`RichTextEditor` — removido "Auto-save ativo"** — texto no rodapé que indicava auto-save não implementado. Removido para não enganar o usuário.
|
|
45
|
+
- **`RichTextEditor` — sync de `value` externo** — adicionado `useEffect([value])` que sincroniza o HTML do editor quando o valor muda externamente e o editor não está em foco (ex.: carregar dados da API após o mount).
|
|
46
|
+
- **`Sidebar` — `<nav>` landmark + `aria-expanded` + `useMemo`** — o root `<div>` foi substituído por `<nav aria-label="Navegação principal" id="sidebar-nav">`; o botão de toggle recebeu `aria-expanded` e `aria-controls`; `navigationItems` foi envolvido em `useMemo([routes, pathname])` e a dependency do effect de overflow foi atualizada de `navigationItems.length` para `navigationItems`.
|
|
47
|
+
- **`DashboardBarChart` — `topOfStack` com `useMemo`** — computação que determinava qual bar recebe radius no topo do stack foi extraída da IIFE inline para `React.useMemo([stacked, chartSeries])`, evitando recalcular a cada render.
|
|
48
|
+
- **Documentação** — corrigidas e atualizadas as docs de `file-upload.md`, `stepper.md`, `pagination.md`, `tree-view.md`, `rich-text-editor.md`, `sidebar.md`, `chart.md` e `hooks.md` para refletir todas as mudanças desta versão.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## [2.1.4] — 2026-05-19
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- **`XerticaAssistant` — decomposição em sub-componentes** — o componente monolítico (1 468 linhas) foi dividido em 9 sub-componentes focados em `parts/`: `AssistantHeader`, `AssistantCollapsedView`, `AssistantTabBar`, `AssistantWelcomeScreen`, `AssistantMessageBubble`, `AssistantTypingIndicator`, `AssistantConversationList`, `AssistantFeedbackDialog` e `AssistantDocumentEditor`. A API pública (`XerticaAssistantProps`) permanece 100% compatível.
|
|
57
|
+
- **`useAudioPlayer` — headless hook** — toda a lógica do `AudioPlayer` foi extraída para `components/media/audio-player/use-audio-player.ts`. O componente `AudioPlayer` agora consome o hook internamente; API pública inalterada. O hook é exportado via `components/hooks/index.ts`.
|
|
58
|
+
- **`useLayoutShortcuts` — headless hook** — registro de atalhos de teclado (Ctrl+B, Ctrl+I) extraído do `LayoutContext` para `components/hooks/use-layout-shortcuts.ts`. Exportado via `components/hooks/index.ts`.
|
|
59
|
+
- **`CustomTooltipContent` — componente compartilhado** — implementação duplicada de tooltip customizado (existia em `sidebar.tsx` e `xertica-assistant.tsx`) consolidada em `components/shared/CustomTooltipContent.tsx`.
|
|
60
|
+
- **`useIsMobile` — fonte única de detecção mobile** — `use-sidebar.ts`, `use-assistant.ts`, `LayoutContext.tsx` e `AudioPlayer.tsx` agora importam de `components/shared/use-mobile.ts` em vez de duplicar a lógica de `matchMedia`.
|
|
61
|
+
- **`utils/color-utils.ts` — utilitários de cor** — funções `hexToRgb`, `hexToRgba` e `isLightColor` extraídas do `BrandColorsContext` para `utils/color-utils.ts` como funções puras reutilizáveis.
|
|
62
|
+
- **`ThemeToggle` — usa `useTheme()`** — substituída referência direta ao `localStorage` pelo hook `useTheme()` do `ThemeContext`.
|
|
63
|
+
- **`types.ts` — fonte única de tipos do assistente** — `Message`, `Conversation`, `Suggestion`, `MockResponse`, `SearchResult`, `SearchSource`, `SearchCommand` e enums relacionados movidos para `components/assistant/xertica-assistant/types.ts`. `xertica-assistant.tsx` e `AssistenteContext.tsx` re-exportam os tipos para backward compatibility.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
10
67
|
## [2.1.3] — 2026-05-16
|
|
11
68
|
|
|
12
69
|
### Added
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Enterprise-grade React design system** built on Tailwind CSS v4, Radix UI, and Lucide Icons — with a robust AI-first documentation layer for precise LLM-driven composition and autonomous agent interaction.
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/xertica-ui)
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
|
|
8
8
|
---
|
package/bin/cli.ts
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { useAssistantMessages } from './use-assistant-messages';
|
|
2
|
+
export type { UseAssistantMessagesProps, UseAssistantMessagesReturn, EvaluationState } from './use-assistant-messages';
|
|
3
|
+
|
|
4
|
+
export { useAssistantConversations } from './use-assistant-conversations';
|
|
5
|
+
export type { UseAssistantConversationsProps, UseAssistantConversationsReturn } from './use-assistant-conversations';
|
|
6
|
+
|
|
7
|
+
export { useAssistantSuggestions, DEFAULT_SUGGESTIONS } from './use-assistant-suggestions';
|
|
8
|
+
export type { UseAssistantSuggestionsProps, UseAssistantSuggestionsReturn } from './use-assistant-suggestions';
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import type { Message, Conversation, SearchResult, AssistantTab } from '../types';
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Props & Return types
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export interface UseAssistantConversationsProps {
|
|
9
|
+
savedConversations?: Conversation[];
|
|
10
|
+
/** Current tab — used to compute conversasFiltradas */
|
|
11
|
+
abaSelecionada: AssistantTab;
|
|
12
|
+
setAbaSelecionada: (tab: AssistantTab) => void;
|
|
13
|
+
setMensagens: React.Dispatch<React.SetStateAction<Message[]>>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UseAssistantConversationsReturn {
|
|
17
|
+
conversas: Conversation[];
|
|
18
|
+
conversaAtual: string | null;
|
|
19
|
+
conversasFiltradas: Conversation[];
|
|
20
|
+
savedSearches: string[];
|
|
21
|
+
executingCommand: string | null;
|
|
22
|
+
handleNovaConversa: () => void;
|
|
23
|
+
handleSelecionarConversa: (conversaId: string) => void;
|
|
24
|
+
handleToggleFavoritaConversa: (conversaId: string) => void;
|
|
25
|
+
handleOpenSearchResult: (result: SearchResult) => void;
|
|
26
|
+
handleExecuteSearchCommand: (
|
|
27
|
+
commandId: string,
|
|
28
|
+
searchTerm: string,
|
|
29
|
+
results: SearchResult[],
|
|
30
|
+
messageId: string,
|
|
31
|
+
) => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
// Hook
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
export function useAssistantConversations({
|
|
39
|
+
savedConversations = [],
|
|
40
|
+
abaSelecionada,
|
|
41
|
+
setAbaSelecionada,
|
|
42
|
+
setMensagens,
|
|
43
|
+
}: UseAssistantConversationsProps): UseAssistantConversationsReturn {
|
|
44
|
+
|
|
45
|
+
const [conversas, setConversas] = useState<Conversation[]>(savedConversations);
|
|
46
|
+
const [conversaAtual, setConversaAtual] = useState<string | null>(null);
|
|
47
|
+
const [savedSearches, setSavedSearches] = useState<string[]>([]);
|
|
48
|
+
const [executingCommand, setExecutingCommand] = useState<string | null>(null);
|
|
49
|
+
|
|
50
|
+
// UC-2: re-sync when the parent updates savedConversations (e.g. after API fetch)
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
setConversas(savedConversations);
|
|
53
|
+
}, [savedConversations]);
|
|
54
|
+
|
|
55
|
+
// UC-4: memoize so it isn't recomputed on every parent re-render
|
|
56
|
+
const conversasFiltradas = useMemo(
|
|
57
|
+
() => conversas.filter(conversa => abaSelecionada === 'favoritos' ? conversa.isFavorite : true),
|
|
58
|
+
[conversas, abaSelecionada]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// UC-5: ref to cancel the command timer if the component unmounts
|
|
62
|
+
const commandTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
63
|
+
useEffect(() => () => clearTimeout(commandTimerRef.current), []);
|
|
64
|
+
|
|
65
|
+
const handleNovaConversa = useCallback(() => {
|
|
66
|
+
setMensagens([]);
|
|
67
|
+
setConversaAtual(null);
|
|
68
|
+
setAbaSelecionada('chat');
|
|
69
|
+
}, [setMensagens, setAbaSelecionada]);
|
|
70
|
+
|
|
71
|
+
const handleSelecionarConversa = useCallback((conversaId: string) => {
|
|
72
|
+
const conversa = conversas.find(c => c.id === conversaId);
|
|
73
|
+
if (conversa) {
|
|
74
|
+
setMensagens(conversa.messages);
|
|
75
|
+
setConversaAtual(conversaId);
|
|
76
|
+
setAbaSelecionada('chat');
|
|
77
|
+
}
|
|
78
|
+
}, [conversas, setMensagens, setAbaSelecionada]);
|
|
79
|
+
|
|
80
|
+
const handleToggleFavoritaConversa = useCallback((conversaId: string) => {
|
|
81
|
+
setConversas(prev =>
|
|
82
|
+
prev.map(conv =>
|
|
83
|
+
conv.id === conversaId ? { ...conv, isFavorite: !conv.isFavorite } : conv
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
const handleOpenSearchResult = useCallback((_result: SearchResult) => {
|
|
89
|
+
// TODO: implement navigation to search result when conversation routing is available
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const handleExecuteSearchCommand = useCallback(async (
|
|
93
|
+
commandId: string,
|
|
94
|
+
searchTerm: string,
|
|
95
|
+
results: SearchResult[],
|
|
96
|
+
messageId: string,
|
|
97
|
+
) => {
|
|
98
|
+
setExecutingCommand(commandId);
|
|
99
|
+
commandTimerRef.current = setTimeout(() => {
|
|
100
|
+
if (commandId === '5') {
|
|
101
|
+
setSavedSearches(prev => [...prev, messageId]);
|
|
102
|
+
}
|
|
103
|
+
setExecutingCommand(null);
|
|
104
|
+
}, 1500);
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
conversas,
|
|
109
|
+
conversaAtual,
|
|
110
|
+
conversasFiltradas,
|
|
111
|
+
savedSearches,
|
|
112
|
+
executingCommand,
|
|
113
|
+
handleNovaConversa,
|
|
114
|
+
handleSelecionarConversa,
|
|
115
|
+
handleToggleFavoritaConversa,
|
|
116
|
+
handleOpenSearchResult,
|
|
117
|
+
handleExecuteSearchCommand,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
|
+
import { toast } from 'sonner';
|
|
3
|
+
import { gerarResposta } from '../../../shared/assistant-utils';
|
|
4
|
+
import type { Message, Suggestion, MockResponse } from '../types';
|
|
5
|
+
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
// Props & Return types
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface UseAssistantMessagesProps {
|
|
11
|
+
initialMessages?: Message[];
|
|
12
|
+
isProcessing?: boolean;
|
|
13
|
+
demoMode?: boolean;
|
|
14
|
+
customResponses?: MockResponse[];
|
|
15
|
+
responseGenerator?: (message: string) => Promise<string | Partial<Message>> | string | Partial<Message>;
|
|
16
|
+
onSendMessage?: (message: string) => void;
|
|
17
|
+
onEvaluation?: (messageId: string, type: 'like' | 'dislike', reason?: string) => void;
|
|
18
|
+
/** Current tab — used to drive auto-scroll */
|
|
19
|
+
abaSelecionada: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface EvaluationState {
|
|
23
|
+
isOpen: boolean;
|
|
24
|
+
messageId: string | null;
|
|
25
|
+
type: 'dislike' | null;
|
|
26
|
+
category?: string | null;
|
|
27
|
+
reason: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UseAssistantMessagesReturn {
|
|
31
|
+
mensagens: Message[];
|
|
32
|
+
setMensagens: React.Dispatch<React.SetStateAction<Message[]>>;
|
|
33
|
+
mensagem: string;
|
|
34
|
+
setMensagem: (value: string) => void;
|
|
35
|
+
copiedId: string | null;
|
|
36
|
+
generatingPodcastId: string | null;
|
|
37
|
+
editingDocument: { content: string; title: string } | null;
|
|
38
|
+
setEditingDocument: (doc: { content: string; title: string } | null) => void;
|
|
39
|
+
evaluationState: EvaluationState;
|
|
40
|
+
setEvaluationState: React.Dispatch<React.SetStateAction<EvaluationState>>;
|
|
41
|
+
messagesEndRef: React.RefObject<HTMLDivElement>;
|
|
42
|
+
handleEnviarMensagem: (arg?: string) => Promise<void>;
|
|
43
|
+
handleToggleFavorite: (messageId: string) => void;
|
|
44
|
+
handleCopyMessage: (content: string, messageId: string) => Promise<void>;
|
|
45
|
+
handleGeneratePodcast: (messageId: string, content: string) => Promise<void>;
|
|
46
|
+
handleDownloadDocument: (content: string, fileName: string) => void;
|
|
47
|
+
handleDownloadPodcast: (audioUrl: string, fileName: string) => void;
|
|
48
|
+
handleEditDocument: (content: string, title: string) => void;
|
|
49
|
+
handleEvaluationClick: (messageId: string, type: 'like' | 'dislike') => void;
|
|
50
|
+
openFeedbackDialog: (messageId: string, category: string | null) => void;
|
|
51
|
+
handleSubmitDislike: () => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
// Hook
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
export function useAssistantMessages({
|
|
59
|
+
initialMessages = [],
|
|
60
|
+
isProcessing = false,
|
|
61
|
+
demoMode = true,
|
|
62
|
+
customResponses = [],
|
|
63
|
+
responseGenerator,
|
|
64
|
+
onSendMessage,
|
|
65
|
+
onEvaluation,
|
|
66
|
+
abaSelecionada,
|
|
67
|
+
}: UseAssistantMessagesProps): UseAssistantMessagesReturn {
|
|
68
|
+
|
|
69
|
+
const [mensagens, setMensagens] = useState<Message[]>(initialMessages);
|
|
70
|
+
const [mensagem, setMensagem] = useState('');
|
|
71
|
+
const [copiedId, setCopiedId] = useState<string | null>(null);
|
|
72
|
+
const [generatingPodcastId, setGeneratingPodcastId] = useState<string | null>(null);
|
|
73
|
+
const [editingDocument, setEditingDocument] = useState<{ content: string; title: string } | null>(null);
|
|
74
|
+
const [evaluationState, setEvaluationState] = useState<EvaluationState>({
|
|
75
|
+
isOpen: false,
|
|
76
|
+
messageId: null,
|
|
77
|
+
type: null,
|
|
78
|
+
category: null,
|
|
79
|
+
reason: '',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
83
|
+
|
|
84
|
+
// Ref to cancel the AI response timer when the component unmounts (UM-1)
|
|
85
|
+
const responseTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
86
|
+
useEffect(() => () => clearTimeout(responseTimerRef.current), []);
|
|
87
|
+
|
|
88
|
+
// Auto-scroll only when the user is already near the bottom (UM-5).
|
|
89
|
+
// Firing unconditionally would forcibly scroll users who have scrolled up to read history.
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (abaSelecionada !== 'chat' || !messagesEndRef.current) return;
|
|
92
|
+
const viewport = messagesEndRef.current.closest('[data-radix-scroll-area-viewport]')
|
|
93
|
+
?? messagesEndRef.current.parentElement;
|
|
94
|
+
if (viewport) {
|
|
95
|
+
const nearBottom =
|
|
96
|
+
viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight < 120;
|
|
97
|
+
if (nearBottom) messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
|
98
|
+
} else {
|
|
99
|
+
// Fallback when no scroll container is found
|
|
100
|
+
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
|
101
|
+
}
|
|
102
|
+
}, [mensagens, abaSelecionada]);
|
|
103
|
+
|
|
104
|
+
// Sync initial messages exactly once on mount (UM-3).
|
|
105
|
+
// Using a ref guard prevents the effect from resetting the conversation every
|
|
106
|
+
// time the parent re-renders and passes a new array reference.
|
|
107
|
+
const hydratedRef = useRef(false);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!hydratedRef.current && initialMessages.length > 0) {
|
|
110
|
+
setMensagens(initialMessages);
|
|
111
|
+
hydratedRef.current = true;
|
|
112
|
+
}
|
|
113
|
+
}, [initialMessages]);
|
|
114
|
+
|
|
115
|
+
const handleEnviarMensagem = useCallback(async (arg?: string) => {
|
|
116
|
+
let msgToSend = mensagem;
|
|
117
|
+
|
|
118
|
+
if (typeof arg === 'string' && !['document', 'podcast', 'search'].includes(arg)) {
|
|
119
|
+
msgToSend = arg;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!msgToSend.trim() || isProcessing) return;
|
|
123
|
+
|
|
124
|
+
const novaMensagem: Message = {
|
|
125
|
+
id: `msg-${Date.now()}`,
|
|
126
|
+
type: 'user',
|
|
127
|
+
content: msgToSend,
|
|
128
|
+
timestamp: new Date(),
|
|
129
|
+
isFavorite: false,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
setMensagens(prev => [...prev, novaMensagem]);
|
|
133
|
+
|
|
134
|
+
if (onSendMessage) {
|
|
135
|
+
onSendMessage(msgToSend);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (demoMode || responseGenerator) {
|
|
139
|
+
const mensagemAtual = msgToSend;
|
|
140
|
+
responseTimerRef.current = setTimeout(async () => {
|
|
141
|
+
let resposta: string | Partial<Message>;
|
|
142
|
+
|
|
143
|
+
if (responseGenerator) {
|
|
144
|
+
resposta = await responseGenerator(mensagemAtual);
|
|
145
|
+
} else {
|
|
146
|
+
resposta = gerarResposta(mensagemAtual, customResponses);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let novaMensagemIA: Message = {
|
|
150
|
+
id: `msg-${Date.now()}-ia`,
|
|
151
|
+
type: 'assistant',
|
|
152
|
+
content: '',
|
|
153
|
+
timestamp: new Date(),
|
|
154
|
+
isFavorite: false,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (typeof resposta === 'string') {
|
|
158
|
+
novaMensagemIA.content = resposta;
|
|
159
|
+
} else {
|
|
160
|
+
novaMensagemIA = { ...novaMensagemIA, ...resposta };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
setMensagens(prev => [...prev, novaMensagemIA]);
|
|
164
|
+
}, 1000 + Math.random() * 1000);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
setMensagem('');
|
|
168
|
+
}, [mensagem, isProcessing, onSendMessage, demoMode, responseGenerator, customResponses]);
|
|
169
|
+
|
|
170
|
+
const handleToggleFavorite = useCallback((messageId: string) => {
|
|
171
|
+
setMensagens(prev =>
|
|
172
|
+
prev.map(msg =>
|
|
173
|
+
msg.id === messageId ? { ...msg, isFavorite: !msg.isFavorite } : msg
|
|
174
|
+
)
|
|
175
|
+
);
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
178
|
+
const handleCopyMessage = useCallback(async (content: string, messageId: string) => {
|
|
179
|
+
try {
|
|
180
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
181
|
+
try {
|
|
182
|
+
await navigator.clipboard.writeText(content);
|
|
183
|
+
setCopiedId(messageId);
|
|
184
|
+
setTimeout(() => setCopiedId(null), 2000);
|
|
185
|
+
} catch (clipboardError: any) {
|
|
186
|
+
if (clipboardError.name === 'NotAllowedError' || clipboardError.message.includes('permissions policy')) {
|
|
187
|
+
throw new Error('Clipboard permission denied, falling back');
|
|
188
|
+
}
|
|
189
|
+
throw clipboardError;
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
throw new Error('Clipboard API not available');
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
try {
|
|
196
|
+
const textArea = document.createElement('textarea');
|
|
197
|
+
textArea.value = content;
|
|
198
|
+
textArea.style.position = 'fixed';
|
|
199
|
+
textArea.style.left = '-999999px';
|
|
200
|
+
textArea.style.top = '-999999px';
|
|
201
|
+
document.body.appendChild(textArea);
|
|
202
|
+
textArea.focus();
|
|
203
|
+
textArea.select();
|
|
204
|
+
const successful = document.execCommand('copy');
|
|
205
|
+
document.body.removeChild(textArea);
|
|
206
|
+
if (successful) {
|
|
207
|
+
setCopiedId(messageId);
|
|
208
|
+
setTimeout(() => setCopiedId(null), 2000);
|
|
209
|
+
}
|
|
210
|
+
} catch (fallbackErr) {
|
|
211
|
+
console.error('Falha ao copiar:', fallbackErr);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}, []);
|
|
215
|
+
|
|
216
|
+
const handleGeneratePodcast = useCallback(async (messageId: string, content: string) => {
|
|
217
|
+
setGeneratingPodcastId(messageId);
|
|
218
|
+
setTimeout(() => {
|
|
219
|
+
setMensagens(prev =>
|
|
220
|
+
prev.map(msg =>
|
|
221
|
+
msg.id === messageId
|
|
222
|
+
? {
|
|
223
|
+
...msg,
|
|
224
|
+
attachmentType: 'podcast' as const,
|
|
225
|
+
attachmentName: 'Podcast - ' + content.substring(0, 30) + '...',
|
|
226
|
+
audioUrl: 'data:audio/mpeg;base64,//uQx...',
|
|
227
|
+
}
|
|
228
|
+
: msg
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
setGeneratingPodcastId(null);
|
|
232
|
+
}, 2000);
|
|
233
|
+
}, []);
|
|
234
|
+
|
|
235
|
+
const handleDownloadDocument = useCallback((content: string, fileName: string) => {
|
|
236
|
+
const blob = new Blob([content], { type: 'text/markdown' });
|
|
237
|
+
const url = URL.createObjectURL(blob);
|
|
238
|
+
const a = document.createElement('a');
|
|
239
|
+
a.href = url;
|
|
240
|
+
a.download = fileName.endsWith('.md') ? fileName : fileName + '.md';
|
|
241
|
+
document.body.appendChild(a);
|
|
242
|
+
a.click();
|
|
243
|
+
document.body.removeChild(a);
|
|
244
|
+
URL.revokeObjectURL(url);
|
|
245
|
+
}, []);
|
|
246
|
+
|
|
247
|
+
const handleDownloadPodcast = useCallback((audioUrl: string, fileName: string) => {
|
|
248
|
+
const a = document.createElement('a');
|
|
249
|
+
a.href = audioUrl;
|
|
250
|
+
a.download = fileName.endsWith('.mp3') ? fileName : fileName + '.mp3';
|
|
251
|
+
document.body.appendChild(a);
|
|
252
|
+
a.click();
|
|
253
|
+
document.body.removeChild(a);
|
|
254
|
+
}, []);
|
|
255
|
+
|
|
256
|
+
const handleEditDocument = useCallback((content: string, title: string) => {
|
|
257
|
+
setEditingDocument({ content, title });
|
|
258
|
+
}, []);
|
|
259
|
+
|
|
260
|
+
const handleEvaluationClick = useCallback((messageId: string, type: 'like' | 'dislike') => {
|
|
261
|
+
if (type === 'like') {
|
|
262
|
+
if (onEvaluation) onEvaluation(messageId, 'like');
|
|
263
|
+
setMensagens(prev => prev.map(m => m.id === messageId ? { ...m, evaluation: 'like' as const } : m));
|
|
264
|
+
toast.success("Obrigado por avaliar positivamente!");
|
|
265
|
+
}
|
|
266
|
+
}, [onEvaluation]);
|
|
267
|
+
|
|
268
|
+
const openFeedbackDialog = useCallback((messageId: string, category: string | null) => {
|
|
269
|
+
setEvaluationState({
|
|
270
|
+
isOpen: true,
|
|
271
|
+
messageId,
|
|
272
|
+
type: 'dislike',
|
|
273
|
+
category,
|
|
274
|
+
reason: '',
|
|
275
|
+
});
|
|
276
|
+
}, []);
|
|
277
|
+
|
|
278
|
+
const handleSubmitDislike = useCallback(() => {
|
|
279
|
+
if (evaluationState.messageId) {
|
|
280
|
+
const finalReason = evaluationState.category
|
|
281
|
+
? (evaluationState.reason ? `${evaluationState.category}: ${evaluationState.reason}` : evaluationState.category)
|
|
282
|
+
: evaluationState.reason;
|
|
283
|
+
|
|
284
|
+
if (onEvaluation) {
|
|
285
|
+
onEvaluation(evaluationState.messageId, 'dislike', finalReason);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
setMensagens(prev =>
|
|
289
|
+
prev.map(m =>
|
|
290
|
+
m.id === evaluationState.messageId
|
|
291
|
+
? { ...m, evaluation: 'dislike' as const, evaluationReason: finalReason }
|
|
292
|
+
: m
|
|
293
|
+
)
|
|
294
|
+
);
|
|
295
|
+
toast.success("Obrigado pelo seu feedback. Vamos melhorar.");
|
|
296
|
+
}
|
|
297
|
+
setEvaluationState({ isOpen: false, messageId: null, type: null, category: null, reason: '' });
|
|
298
|
+
}, [evaluationState, onEvaluation]);
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
mensagens,
|
|
302
|
+
setMensagens,
|
|
303
|
+
mensagem,
|
|
304
|
+
setMensagem,
|
|
305
|
+
copiedId,
|
|
306
|
+
generatingPodcastId,
|
|
307
|
+
editingDocument,
|
|
308
|
+
setEditingDocument,
|
|
309
|
+
evaluationState,
|
|
310
|
+
setEvaluationState,
|
|
311
|
+
messagesEndRef,
|
|
312
|
+
handleEnviarMensagem,
|
|
313
|
+
handleToggleFavorite,
|
|
314
|
+
handleCopyMessage,
|
|
315
|
+
handleGeneratePodcast,
|
|
316
|
+
handleDownloadDocument,
|
|
317
|
+
handleDownloadPodcast,
|
|
318
|
+
handleEditDocument,
|
|
319
|
+
handleEvaluationClick,
|
|
320
|
+
openFeedbackDialog,
|
|
321
|
+
handleSubmitDislike,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import type { Suggestion } from '../types';
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Default suggestions
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_SUGGESTIONS: Suggestion[] = [
|
|
9
|
+
{ id: '1', text: 'Me ajude a criar um documento profissional' },
|
|
10
|
+
{ id: '2', text: 'Buscar nos meus arquivos por "relatório"' },
|
|
11
|
+
{ id: '3', text: 'Resuma as conversas importantes desta semana' },
|
|
12
|
+
{ id: '4', text: 'Crie um podcast sobre o último projeto' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
// Props & Return types
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export interface UseAssistantSuggestionsProps {
|
|
20
|
+
suggestions?: Suggestion[];
|
|
21
|
+
richSuggestions?: Suggestion[];
|
|
22
|
+
onRichAction?: (actionId: string, actionText: string) => void;
|
|
23
|
+
/** handleEnviarMensagem from useAssistantMessages, passed in to avoid circular deps */
|
|
24
|
+
handleEnviarMensagem: (arg?: string | any) => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UseAssistantSuggestionsReturn {
|
|
28
|
+
sugestoes: Suggestion[];
|
|
29
|
+
showMoreSuggestions: boolean;
|
|
30
|
+
setShowMoreSuggestions: (show: boolean) => void;
|
|
31
|
+
handleRichSuggestionClick: (suggestion: Suggestion) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
// Hook
|
|
36
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
export function useAssistantSuggestions({
|
|
39
|
+
suggestions,
|
|
40
|
+
richSuggestions = [],
|
|
41
|
+
onRichAction,
|
|
42
|
+
handleEnviarMensagem,
|
|
43
|
+
}: UseAssistantSuggestionsProps): UseAssistantSuggestionsReturn {
|
|
44
|
+
|
|
45
|
+
const [showMoreSuggestions, setShowMoreSuggestions] = useState(false);
|
|
46
|
+
|
|
47
|
+
const sugestoes = suggestions ?? DEFAULT_SUGGESTIONS;
|
|
48
|
+
|
|
49
|
+
const handleRichSuggestionClick = useCallback((suggestion: Suggestion) => {
|
|
50
|
+
if (onRichAction) {
|
|
51
|
+
onRichAction(suggestion.id, suggestion.text);
|
|
52
|
+
} else {
|
|
53
|
+
handleEnviarMensagem(suggestion.text);
|
|
54
|
+
}
|
|
55
|
+
setShowMoreSuggestions(false);
|
|
56
|
+
}, [onRichAction, handleEnviarMensagem]);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
sugestoes,
|
|
60
|
+
showMoreSuggestions,
|
|
61
|
+
setShowMoreSuggestions,
|
|
62
|
+
handleRichSuggestionClick,
|
|
63
|
+
};
|
|
64
|
+
}
|