@orbe-agro/client-core 5.6.260 → 5.6.261

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 (94) hide show
  1. package/dist/@ecme/assets/styles/app.css +23 -3
  2. package/dist/@ecme/assets/styles/tailwind/index.css +25 -3
  3. package/dist/@ecme/components/template/Footer.js +1 -1
  4. package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiChatDrawer.js +121 -0
  5. package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiChatDrawer.js.map +1 -0
  6. package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.js +47 -18
  7. package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.js.map +1 -1
  8. package/dist/@ecme/components/template/OrbeAiWidget/useResizablePanel.js +45 -31
  9. package/dist/@ecme/components/template/OrbeAiWidget/useResizablePanel.js.map +1 -1
  10. package/dist/@ecme/components/ui/Dropdown/DropdownMenu.js +5 -1
  11. package/dist/@ecme/components/ui/Dropdown/DropdownMenu.js.map +1 -1
  12. package/dist/@ecme/index.js +4 -1
  13. package/dist/@ecme/services/AiService.js +22 -0
  14. package/dist/@ecme/services/AiService.js.map +1 -1
  15. package/dist/@ecme/services/index.js +4 -1
  16. package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistory.js +9 -23
  17. package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistory.js.map +1 -1
  18. package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistoryItem.js +6 -16
  19. package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistoryItem.js.map +1 -1
  20. package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistoryRenameDialog.js +10 -7
  21. package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistoryRenameDialog.js.map +1 -1
  22. package/dist/@ecme/views/concepts/ai/Chat/components/ChatView.js +6 -4
  23. package/dist/@ecme/views/concepts/ai/Chat/components/ChatView.js.map +1 -1
  24. package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatConversations.js +83 -0
  25. package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatConversations.js.map +1 -0
  26. package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatSend.js +3 -2
  27. package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatSend.js.map +1 -1
  28. package/dist/@ecme/views/concepts/ai/Chat/hooks/useConversationMessages.js +37 -0
  29. package/dist/@ecme/views/concepts/ai/Chat/hooks/useConversationMessages.js.map +1 -0
  30. package/dist/@ecme/views/concepts/ai/Chat/store/generativeChatStore.js +25 -1
  31. package/dist/@ecme/views/concepts/ai/Chat/store/generativeChatStore.js.map +1 -1
  32. package/dist/@ecme/views/concepts/ai/Chat/utils.js +16 -1
  33. package/dist/@ecme/views/concepts/ai/Chat/utils.js.map +1 -1
  34. package/dist/@types/@ecme/components/template/OrbeAiWidget/OrbeAiChatDrawer.d.ts +14 -0
  35. package/dist/@types/@ecme/components/template/OrbeAiWidget/OrbeAiChatDrawer.d.ts.map +1 -0
  36. package/dist/@types/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.d.ts.map +1 -1
  37. package/dist/@types/@ecme/components/template/OrbeAiWidget/useResizablePanel.d.ts +12 -5
  38. package/dist/@types/@ecme/components/template/OrbeAiWidget/useResizablePanel.d.ts.map +1 -1
  39. package/dist/@types/@ecme/components/ui/Dropdown/DropdownMenu.d.ts +2 -0
  40. package/dist/@types/@ecme/components/ui/Dropdown/DropdownMenu.d.ts.map +1 -1
  41. package/dist/@types/@ecme/services/AiService.d.ts +5 -0
  42. package/dist/@types/@ecme/services/AiService.d.ts.map +1 -1
  43. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistory.d.ts +3 -1
  44. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistory.d.ts.map +1 -1
  45. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistoryItem.d.ts +1 -1
  46. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistoryItem.d.ts.map +1 -1
  47. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistoryRenameDialog.d.ts +5 -1
  48. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistoryRenameDialog.d.ts.map +1 -1
  49. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatView.d.ts +8 -1
  50. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatView.d.ts.map +1 -1
  51. package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useChatConversations.d.ts +12 -0
  52. package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useChatConversations.d.ts.map +1 -0
  53. package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useChatSend.d.ts.map +1 -1
  54. package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useConversationMessages.d.ts +12 -0
  55. package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useConversationMessages.d.ts.map +1 -0
  56. package/dist/@types/@ecme/views/concepts/ai/Chat/store/generativeChatStore.d.ts +4 -0
  57. package/dist/@types/@ecme/views/concepts/ai/Chat/store/generativeChatStore.d.ts.map +1 -1
  58. package/dist/@types/@ecme/views/concepts/ai/Chat/types.d.ts +24 -0
  59. package/dist/@types/@ecme/views/concepts/ai/Chat/types.d.ts.map +1 -1
  60. package/dist/@types/@ecme/views/concepts/ai/Chat/utils.d.ts +13 -0
  61. package/dist/@types/@ecme/views/concepts/ai/Chat/utils.d.ts.map +1 -1
  62. package/dist/@types/base/configs/endpoints.config/endpoints.navigation.d.ts +8 -0
  63. package/dist/@types/base/configs/endpoints.config/endpoints.navigation.d.ts.map +1 -1
  64. package/dist/@types/base/configs/endpoints.config/transportadora/contratoFrete.d.ts +8 -0
  65. package/dist/@types/base/configs/endpoints.config/transportadora/contratoFrete.d.ts.map +1 -1
  66. package/dist/@types/base/services/modules/transportadora/ContratoFrete/ContratoFreteService.d.ts +2 -0
  67. package/dist/@types/base/services/modules/transportadora/ContratoFrete/ContratoFreteService.d.ts.map +1 -1
  68. package/dist/base/assets/styles/init.css +23 -4
  69. package/dist/base/configs/endpoints.config/transportadora/contratoFrete.js +3 -1
  70. package/dist/base/configs/endpoints.config/transportadora/contratoFrete.js.map +1 -1
  71. package/dist/base/index.js +3 -1
  72. package/dist/base/services/index.js +3 -1
  73. package/dist/base/services/modules/index.js +3 -1
  74. package/dist/base/services/modules/transportadora/ContratoFrete/ContratoFreteService.js +14 -0
  75. package/dist/base/services/modules/transportadora/ContratoFrete/ContratoFreteService.js.map +1 -1
  76. package/dist/base/services/modules/transportadora/index.js +3 -1
  77. package/lib/@ecme/components/template/OrbeAiWidget/OrbeAiChatDrawer.tsx +140 -0
  78. package/lib/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.tsx +48 -16
  79. package/lib/@ecme/components/template/OrbeAiWidget/useResizablePanel.ts +72 -37
  80. package/lib/@ecme/components/ui/Dropdown/DropdownMenu.tsx +7 -1
  81. package/lib/@ecme/services/AiService.ts +24 -0
  82. package/lib/@ecme/views/concepts/ai/Chat/components/ChatHistory.tsx +11 -27
  83. package/lib/@ecme/views/concepts/ai/Chat/components/ChatHistoryItem.tsx +7 -13
  84. package/lib/@ecme/views/concepts/ai/Chat/components/ChatHistoryRenameDialog.tsx +17 -8
  85. package/lib/@ecme/views/concepts/ai/Chat/components/ChatView.tsx +17 -4
  86. package/lib/@ecme/views/concepts/ai/Chat/hooks/useChatConversations.tsx +128 -0
  87. package/lib/@ecme/views/concepts/ai/Chat/hooks/useChatSend.ts +3 -2
  88. package/lib/@ecme/views/concepts/ai/Chat/hooks/useConversationMessages.ts +68 -0
  89. package/lib/@ecme/views/concepts/ai/Chat/store/generativeChatStore.ts +44 -1
  90. package/lib/@ecme/views/concepts/ai/Chat/types.ts +28 -0
  91. package/lib/@ecme/views/concepts/ai/Chat/utils.ts +33 -0
  92. package/lib/base/configs/endpoints.config/transportadora/contratoFrete.ts +2 -0
  93. package/lib/base/services/modules/transportadora/ContratoFrete/ContratoFreteService.ts +14 -0
  94. package/package.json +1 -1
@@ -1,15 +1,17 @@
1
- import { useState } from 'react'
1
+ import { useCallback, useState } from 'react'
2
2
  import { AnimatePresence, motion } from 'framer-motion'
3
3
  import {
4
4
  TbMessageChatbot,
5
5
  TbArrowsMaximize,
6
6
  TbArrowsMinimize,
7
+ TbMenu2,
7
8
  } from 'react-icons/tb'
8
9
  import { HiX } from 'react-icons/hi'
9
10
  import ChatView from '@/views/concepts/ai/Chat/components/ChatView'
10
11
  import ChatHistoryRenameDialog from '@/views/concepts/ai/Chat/components/ChatHistoryRenameDialog'
11
- import useResizablePanel from './useResizablePanel'
12
+ import useResizablePanel, { MARGIN } from './useResizablePanel'
12
13
  import ResizeHandles from './ResizeHandles'
14
+ import OrbeAiChatDrawer from './OrbeAiChatDrawer'
13
15
 
14
16
  const iconButtonClass =
15
17
  'flex h-8 w-8 items-center justify-center rounded-full text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200'
@@ -17,27 +19,43 @@ const iconButtonClass =
17
19
  const OrbeAiWidget = () => {
18
20
  const [open, setOpen] = useState(false)
19
21
  const [fullscreen, setFullscreen] = useState(false)
20
- const { rect, startResize } = useResizablePanel()
22
+ const [drawerOpen, setDrawerOpen] = useState(false)
23
+ const { rect, startResize, anchor } = useResizablePanel()
21
24
 
22
- const toggleOpen = () => setOpen((prev) => !prev)
25
+ const toggleOpen = () => {
26
+ // Sempre abre colado no botão. No mesmo clique do setOpen: o React junta
27
+ // os dois updates e o painel não pisca na posição antiga.
28
+ if (!open) anchor()
29
+ setOpen((prev) => !prev)
30
+ setDrawerOpen(false)
31
+ }
23
32
  const toggleFullscreen = () => setFullscreen((prev) => !prev)
33
+ const toggleDrawer = () => setDrawerOpen((prev) => !prev)
34
+ // Estável: o drawer registra o listener de Esc com ela como dependência.
35
+ const closeDrawer = useCallback(() => setDrawerOpen(false), [])
24
36
  const close = () => {
25
37
  setOpen(false)
26
38
  setFullscreen(false)
39
+ setDrawerOpen(false)
27
40
  }
28
41
 
29
- // Tela cheia: cobre o viewport inteiro (inset-0). Flutuante: usa o retângulo do resize.
42
+ // Tela cheia: cobre o viewport inteiro (inset-0). Flutuante: ancorado no
43
+ // canto do botão (right/bottom). O max-* limita só a exibição — o estado
44
+ // guarda o tamanho escolhido, então ao desfazer o zoom o painel volta igual.
45
+ // `100%` de um `fixed` é o viewport sem a barra de rolagem.
30
46
  const panelStyle = fullscreen
31
47
  ? undefined
32
48
  : {
33
- top: rect.top,
34
- left: rect.left,
49
+ right: rect.right,
50
+ bottom: rect.bottom,
35
51
  width: rect.width,
36
52
  height: rect.height,
53
+ maxWidth: `calc(100% - ${rect.right + MARGIN}px)`,
54
+ maxHeight: `calc(100% - ${rect.bottom + MARGIN}px)`,
37
55
  }
38
56
  const panelVariantClass = fullscreen
39
57
  ? 'inset-0 rounded-none border-0'
40
- : 'rounded-2xl border shadow-2xl'
58
+ : 'origin-bottom-right rounded-2xl border shadow-2xl'
41
59
 
42
60
  return (
43
61
  <div
@@ -60,16 +78,25 @@ const OrbeAiWidget = () => {
60
78
  <ResizeHandles onResizeStart={startResize} />
61
79
  )}
62
80
 
63
- <div className="flex items-center justify-between border-b border-gray-200 px-4 py-3 dark:border-gray-700">
64
- <div className="flex items-center gap-2">
65
- <span className="flex h-8 w-8 items-center justify-center rounded-full bg-primary text-white">
81
+ <div className="flex items-center justify-between gap-2 border-b border-gray-200 px-3 py-3 sm:px-4 dark:border-gray-700">
82
+ <div className="flex min-w-0 items-center gap-1 sm:gap-2">
83
+ <button
84
+ type="button"
85
+ className={iconButtonClass}
86
+ onClick={toggleDrawer}
87
+ aria-label="Abrir conversas"
88
+ aria-expanded={drawerOpen}
89
+ >
90
+ <TbMenu2 size={20} />
91
+ </button>
92
+ <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary text-white">
66
93
  <TbMessageChatbot size={20} />
67
94
  </span>
68
- <span className="font-semibold text-gray-900 dark:text-gray-100">
95
+ <span className="truncate font-semibold text-gray-900 dark:text-gray-100">
69
96
  Orbe AI
70
97
  </span>
71
98
  </div>
72
- <div className="flex items-center gap-2">
99
+ <div className="flex shrink-0 items-center gap-1 sm:gap-2">
73
100
  <button
74
101
  type="button"
75
102
  className={iconButtonClass}
@@ -98,9 +125,14 @@ const OrbeAiWidget = () => {
98
125
  </div>
99
126
 
100
127
  <div className="min-h-0 flex-1 p-2">
101
- <ChatView />
102
- <ChatHistoryRenameDialog />
128
+ <ChatView showMobileNav={false} />
129
+ <ChatHistoryRenameDialog overlayClassName="!z-[10000]" />
103
130
  </div>
131
+
132
+ <OrbeAiChatDrawer
133
+ open={drawerOpen}
134
+ onClose={closeDrawer}
135
+ />
104
136
  </motion.div>
105
137
  )}
106
138
  </AnimatePresence>
@@ -108,7 +140,7 @@ const OrbeAiWidget = () => {
108
140
  {!fullscreen && (
109
141
  <button
110
142
  type="button"
111
- className="flex h-14 w-14 items-center justify-center rounded-full bg-primary text-white shadow-lg transition-colors hover:bg-primary-deep focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
143
+ className="flex h-14 w-14 items-center justify-center rounded-full bg-primary text-neutral shadow-lg transition-colors hover:bg-primary-mild focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
112
144
  onClick={toggleOpen}
113
145
  aria-label={open ? 'Fechar chat Orbe AI' : 'Abrir chat Orbe AI'}
114
146
  aria-expanded={open}
@@ -1,7 +1,12 @@
1
- import { useCallback, useState } from 'react'
1
+ import { useCallback, useEffect, useState } from 'react'
2
2
  import type { PointerEvent as ReactPointerEvent } from 'react'
3
3
 
4
- export type Rect = { top: number; left: number; width: number; height: number }
4
+ /**
5
+ * `right`/`bottom` são offsets a partir das bordas direita/inferior do
6
+ * viewport — a mesma referência do botão (FAB, `bottom-6 right-6`). Assim,
7
+ * resolução e zoom movem painel e botão juntos.
8
+ */
9
+ export type Rect = { right: number; bottom: number; width: number; height: number }
5
10
 
6
11
  export type ResizeDirection =
7
12
  | 'top'
@@ -15,31 +20,47 @@ export type ResizeDirection =
15
20
 
16
21
  const MIN = { width: 360, height: 420 }
17
22
  const DEFAULT = { width: 480, height: 720 }
18
- const MARGIN = 12 // distância mínima das bordas do viewport
19
- const ANCHOR = { right: 24, bottomOffset: 92 } // canto inferior-direito, acima do FAB
23
+ export const MARGIN = 12 // distância mínima das bordas do viewport
20
24
 
25
+ /**
26
+ * Canto inferior-direito do painel, logo acima do FAB:
27
+ * right = `right-6` (24); bottom = `bottom-6` (24) + `h-14` (56) + `gap-3` (12).
28
+ * Se as classes do botão no OrbeAiWidget mudarem, ajustar aqui.
29
+ */
30
+ const ANCHOR = { right: 24, bottom: 92 }
31
+
32
+ /** Em telas menores que o MIN o máximo vence: o painel nunca sai da tela. */
21
33
  const clamp = (value: number, min: number, max: number) =>
22
34
  Math.min(Math.max(value, min), max)
23
35
 
24
- /** Retângulo inicial: ancorado no canto inferior-direito, logo acima do FAB. */
25
- const getDefaultRect = (): Rect => {
26
- const width = Math.min(DEFAULT.width, window.innerWidth - MARGIN * 2)
27
- const height = Math.min(DEFAULT.height, window.innerHeight - MARGIN * 2)
28
- return {
29
- width,
30
- height,
31
- left: Math.max(MARGIN, window.innerWidth - width - ANCHOR.right),
32
- top: Math.max(MARGIN, window.innerHeight - height - ANCHOR.bottomOffset),
33
- }
34
- }
36
+ /** Área útil do `fixed`: o viewport sem a barra de rolagem, igual ao `100%` do CSS. */
37
+ const getViewport = () => ({
38
+ width: document.documentElement.clientWidth,
39
+ height: document.documentElement.clientHeight,
40
+ })
35
41
 
36
42
  /**
37
- * Painel redimensionável por qualquer borda/canto. O painel é posicionado
38
- * livremente (`fixed` com top/left/width/height); cada direção move sua borda
39
- * mantendo a borda oposta fixa, com limites de mínimo e do viewport.
43
+ * Painel redimensionável por qualquer borda/canto, ancorado no botão. Cada
44
+ * direção move sua borda mantendo a oposta fixa. O tamanho nunca é alterado
45
+ * por mudança de viewport: quem limita é o max-width/max-height do painel.
40
46
  */
41
47
  const useResizablePanel = () => {
42
- const [rect, setRect] = useState<Rect>(getDefaultRect)
48
+ const [rect, setRect] = useState<Rect>({ ...ANCHOR, ...DEFAULT })
49
+
50
+ /** Recola o painel acima do botão, mantendo o tamanho escolhido. */
51
+ const anchor = useCallback(() => {
52
+ setRect((prev) =>
53
+ prev.right === ANCHOR.right && prev.bottom === ANCHOR.bottom
54
+ ? prev
55
+ : { ...prev, ...ANCHOR },
56
+ )
57
+ }, [])
58
+
59
+ // Resolução e zoom disparam `resize`: o painel volta para cima do botão.
60
+ useEffect(() => {
61
+ window.addEventListener('resize', anchor)
62
+ return () => window.removeEventListener('resize', anchor)
63
+ }, [anchor])
43
64
 
44
65
  const startResize = useCallback(
45
66
  (event: ReactPointerEvent, direction: ResizeDirection) => {
@@ -48,9 +69,17 @@ const useResizablePanel = () => {
48
69
 
49
70
  const startX = event.clientX
50
71
  const startY = event.clientY
51
- const start = rect
52
- const right = start.left + start.width
53
- const bottom = start.top + start.height
72
+ const viewport = getViewport()
73
+ // Parte do tamanho RENDERIZADO: se o CSS está limitando o painel, o
74
+ // valor guardado é maior que o visível e o arrasto teria zona morta.
75
+ const start: Rect = {
76
+ ...rect,
77
+ width: Math.min(rect.width, viewport.width - rect.right - MARGIN),
78
+ height: Math.min(
79
+ rect.height,
80
+ viewport.height - rect.bottom - MARGIN,
81
+ ),
82
+ }
54
83
 
55
84
  const previousUserSelect = document.body.style.userSelect
56
85
  document.body.style.userSelect = 'none'
@@ -60,31 +89,37 @@ const useResizablePanel = () => {
60
89
  const dy = moveEvent.clientY - startY
61
90
  const next: Rect = { ...start }
62
91
 
63
- if (direction.includes('right')) {
92
+ // Esquerda/topo: a borda ancorada fica fixa, só o tamanho muda.
93
+ if (direction.includes('left')) {
64
94
  next.width = clamp(
65
- start.width + dx,
95
+ start.width - dx,
66
96
  MIN.width,
67
- window.innerWidth - start.left - MARGIN,
97
+ viewport.width - start.right - MARGIN,
68
98
  )
69
99
  }
70
- if (direction.includes('left')) {
71
- next.width = clamp(start.width - dx, MIN.width, right - MARGIN)
72
- next.left = right - next.width
73
- }
74
- if (direction.includes('bottom')) {
100
+ if (direction.includes('top')) {
75
101
  next.height = clamp(
76
- start.height + dy,
102
+ start.height - dy,
77
103
  MIN.height,
78
- window.innerHeight - start.top - MARGIN,
104
+ viewport.height - start.bottom - MARGIN,
79
105
  )
80
106
  }
81
- if (direction.includes('top')) {
107
+ // Direita/baixo: a borda oposta fica fixa, então o offset anda junto.
108
+ if (direction.includes('right')) {
109
+ next.width = clamp(
110
+ start.width + dx,
111
+ MIN.width,
112
+ start.width + start.right - MARGIN,
113
+ )
114
+ next.right = start.right - (next.width - start.width)
115
+ }
116
+ if (direction.includes('bottom')) {
82
117
  next.height = clamp(
83
- start.height - dy,
118
+ start.height + dy,
84
119
  MIN.height,
85
- bottom - MARGIN,
120
+ start.height + start.bottom - MARGIN,
86
121
  )
87
- next.top = bottom - next.height
122
+ next.bottom = start.bottom - (next.height - start.height)
88
123
  }
89
124
 
90
125
  setRect(next)
@@ -102,7 +137,7 @@ const useResizablePanel = () => {
102
137
  [rect],
103
138
  )
104
139
 
105
- return { rect, startResize }
140
+ return { rect, startResize, anchor }
106
141
  }
107
142
 
108
143
  export default useResizablePanel
@@ -47,6 +47,8 @@ export interface DropdownMenuProps
47
47
  activeKey?: string
48
48
  title?: string | ReactNode
49
49
  menuClass?: string
50
+ /** Classes do wrapper flutuante (portal). Útil para subir o z-index acima de painéis fixos. */
51
+ floatingClass?: string
50
52
  trigger?: 'click' | 'hover' | 'context'
51
53
  placement?: Placement
52
54
  onOpen?: (bool: boolean) => void
@@ -66,6 +68,7 @@ const DropdownMenu = (props: DropdownMenuProps & HTMLProps<HTMLElement>) => {
66
68
  disabled,
67
69
  toggleClassName,
68
70
  menuClass,
71
+ floatingClass,
69
72
  placement,
70
73
  trigger = 'click',
71
74
  onOpen,
@@ -302,7 +305,10 @@ const DropdownMenu = (props: DropdownMenuProps & HTMLProps<HTMLElement>) => {
302
305
  ref={refs.setFloating}
303
306
  style={floatingStyles}
304
307
  {...getFloatingProps()}
305
- className="outline-none z-30"
308
+ className={classNames(
309
+ 'outline-none z-30',
310
+ floatingClass,
311
+ )}
306
312
  >
307
313
  <ul
308
314
  className={classNames(
@@ -49,6 +49,30 @@ export async function apiGetChatHistory<T>(conversationId?: string) {
49
49
  })
50
50
  }
51
51
 
52
+ /** Conversas do usuário (só o resumo), a mais recente primeiro. */
53
+ export async function apiGetConversations<T>() {
54
+ return aiRequest<T>({
55
+ url: '/ai/chat/conversations',
56
+ method: 'get',
57
+ })
58
+ }
59
+
60
+ export async function apiRenameConversation(conversationId: string, title: string) {
61
+ return aiRequest<void>({
62
+ url: `/ai/chat/conversations/${encodeURIComponent(conversationId)}`,
63
+ method: 'patch',
64
+ data: { title },
65
+ })
66
+ }
67
+
68
+ /** Apaga a conversa e toda a memória dela no backend. */
69
+ export async function apiDeleteConversation(conversationId: string) {
70
+ return aiRequest<void>({
71
+ url: `/ai/chat/conversations/${encodeURIComponent(conversationId)}`,
72
+ method: 'delete',
73
+ })
74
+ }
75
+
52
76
  export type StreamChatParams = {
53
77
  prompt: string
54
78
  /** Identifica a conversa; o backend usa como thread da memoria. */
@@ -2,46 +2,30 @@ import { Fragment } from 'react'
2
2
  import ScrollBar from '@/components/ui/ScrollBar'
3
3
  import ChatHistoryItem from './ChatHistoryItem'
4
4
  import { usGenerativeChatStore } from '../store/generativeChatStore'
5
- import { apiGetChatHistory } from '@/services/AiService'
6
-
7
- import useSWR from 'swr'
8
- import type { GetChatHistoryResponse } from '../types'
5
+ import useChatConversations from '../hooks/useChatConversations'
9
6
 
10
7
  type ChatHistoryProps = {
11
8
  queryText?: string
12
9
  onClick?: () => void
10
+ /** Repassado ao menu de cada item (ex.: subir o z-index dentro do widget). */
11
+ dropdownFloatingClass?: string
13
12
  }
14
13
 
15
- const ChatHistory = ({ queryText = '', onClick }: ChatHistoryProps) => {
14
+ const ChatHistory = ({
15
+ queryText = '',
16
+ onClick,
17
+ dropdownFloatingClass,
18
+ }: ChatHistoryProps) => {
16
19
  const {
17
20
  chatHistory,
18
- setChatHistory,
19
21
  setRenameDialog,
20
22
  setSelectedConversation,
21
23
  selectedConversation,
22
24
  } = usGenerativeChatStore()
23
-
24
- useSWR(
25
- ['/api/ai/chat/history'],
26
- () => apiGetChatHistory<GetChatHistoryResponse>(),
27
- {
28
- revalidateOnFocus: false,
29
- revalidateIfStale: false,
30
- revalidateOnReconnect: false,
31
- onSuccess: (data) => {
32
- setChatHistory(data)
33
- },
34
- },
35
- )
25
+ const { remover } = useChatConversations()
36
26
 
37
27
  const handleDelete = (id: string) => {
38
- setChatHistory(chatHistory.filter((item) => item.id !== id))
39
- setSelectedConversation('')
40
- }
41
-
42
- const handleArchive = (id: string) => {
43
- setChatHistory(chatHistory.filter((item) => item.id !== id))
44
- setSelectedConversation('')
28
+ remover(id)
45
29
  }
46
30
 
47
31
  const handleRename = (id: string, title: string) => {
@@ -77,8 +61,8 @@ const ChatHistory = ({ queryText = '', onClick }: ChatHistoryProps) => {
77
61
  title={item.title}
78
62
  conversation={item.lastConversation}
79
63
  active={selectedConversation === item.id}
64
+ dropdownFloatingClass={dropdownFloatingClass}
80
65
  onDelete={() => handleDelete(item.id)}
81
- onArchive={() => handleArchive(item.id)}
82
66
  onRename={() =>
83
67
  handleRename(item.id, item.title)
84
68
  }
@@ -1,7 +1,7 @@
1
1
  import { useState, useRef } from 'react'
2
2
  import Dropdown from '@/components/ui/Dropdown'
3
3
  import classNames from 'classnames'
4
- import { TbDots, TbArchive, TbPencil, TbTrash } from 'react-icons/tb'
4
+ import { TbDots, TbPencil, TbTrash } from 'react-icons/tb'
5
5
  import type { DropdownRef } from '@/components/ui/Dropdown'
6
6
 
7
7
  import type { MouseEvent, SyntheticEvent } from 'react'
@@ -11,9 +11,9 @@ type ChatHistoryItemProps = {
11
11
  conversation: string
12
12
  active?: boolean
13
13
  onDelete?: () => void
14
- onArchive?: () => void
15
14
  onRename?: () => void
16
15
  onClick?: () => void
16
+ dropdownFloatingClass?: string
17
17
  }
18
18
 
19
19
  const ChatHistoryItem = (props: ChatHistoryItemProps) => {
@@ -26,9 +26,9 @@ const ChatHistoryItem = (props: ChatHistoryItemProps) => {
26
26
  conversation,
27
27
  active,
28
28
  onRename,
29
- onArchive,
30
29
  onDelete,
31
30
  onClick,
31
+ dropdownFloatingClass,
32
32
  ...rest
33
33
  } = props
34
34
 
@@ -68,6 +68,7 @@ const ChatHistoryItem = (props: ChatHistoryItemProps) => {
68
68
  <Dropdown
69
69
  ref={dropdownRef}
70
70
  placement="bottom-end"
71
+ floatingClass={dropdownFloatingClass}
71
72
  renderTitle={
72
73
  <div
73
74
  className={classNames(
@@ -88,22 +89,15 @@ const ChatHistoryItem = (props: ChatHistoryItemProps) => {
88
89
  onClick={(e) => handleCallback(e, onRename)}
89
90
  >
90
91
  <TbPencil className="text-xl" />
91
- <span>Rename</span>
92
- </Dropdown.Item>
93
- <Dropdown.Item
94
- eventKey="rename"
95
- onClick={(e) => handleCallback(e, onArchive)}
96
- >
97
- <TbArchive className="text-xl" />
98
- <span>Archive</span>
92
+ <span>Renomear</span>
99
93
  </Dropdown.Item>
100
94
  <Dropdown.Item
101
95
  className="text-error"
102
- eventKey="rename"
96
+ eventKey="delete"
103
97
  onClick={(e) => handleCallback(e, onDelete)}
104
98
  >
105
99
  <TbTrash className="text-xl text-error" />
106
- <span className="text-error">Deleted</span>
100
+ <span className="text-error">Excluir</span>
107
101
  </Dropdown.Item>
108
102
  </Dropdown>
109
103
  </div>
@@ -4,6 +4,7 @@ import { Form, FormItem } from '@/components/ui/Form'
4
4
  import Input from '@/components/ui/Input'
5
5
  import Button from '@/components/ui/Button'
6
6
  import { usGenerativeChatStore } from '../store/generativeChatStore'
7
+ import useChatConversations from '../hooks/useChatConversations'
7
8
  import { useForm, Controller } from 'react-hook-form'
8
9
  import { zodResolver } from '@hookform/resolvers/zod'
9
10
  import { z } from 'zod'
@@ -14,12 +15,19 @@ type FormSchema = {
14
15
  }
15
16
 
16
17
  const validationSchema: ZodType<FormSchema> = z.object({
17
- title: z.string().min(1, 'Please do not leave chat title blank!'),
18
+ title: z.string().trim().min(1, 'Informe um título para a conversa.'),
18
19
  })
19
20
 
20
- const ChatHistoryRenameDialog = () => {
21
- const { renameDialog, setRenameDialog, setChatHistoryName } =
22
- usGenerativeChatStore()
21
+ type ChatHistoryRenameDialogProps = {
22
+ /** Classes do overlay (ex.: subir o z-index acima do widget flutuante). */
23
+ overlayClassName?: string
24
+ }
25
+
26
+ const ChatHistoryRenameDialog = ({
27
+ overlayClassName,
28
+ }: ChatHistoryRenameDialogProps) => {
29
+ const { renameDialog, setRenameDialog } = usGenerativeChatStore()
30
+ const { renomear } = useChatConversations()
23
31
 
24
32
  const {
25
33
  control,
@@ -51,16 +59,17 @@ const ChatHistoryRenameDialog = () => {
51
59
  }
52
60
 
53
61
  const onFormSubmit = async ({ title }: FormSchema) => {
54
- setChatHistoryName({
55
- id: renameDialog.id,
56
- title,
57
- })
62
+ // Fecha antes da resposta: o rename é otimista e desfaz sozinho se o
63
+ // backend recusar.
64
+ const { id } = renameDialog
58
65
  handleDialogClose()
66
+ await renomear(id, title.trim())
59
67
  }
60
68
 
61
69
  return (
62
70
  <Dialog
63
71
  isOpen={renameDialog.open}
72
+ overlayClassName={overlayClassName}
64
73
  onClose={handleDialogClose}
65
74
  onRequestClose={handleDialogClose}
66
75
  >
@@ -12,6 +12,7 @@ import ApprovalCard from './ApprovalCard'
12
12
  import DataRequestCard from './DataRequestCard'
13
13
  import { usGenerativeChatStore } from '../store/generativeChatStore'
14
14
  import useChatSend from '../hooks/useChatSend'
15
+ import useConversationMessages from '../hooks/useConversationMessages'
15
16
  import type { Conversation } from '../types'
16
17
  import type { ScrollBarRef } from '@/components/ui/ScrollBar'
17
18
 
@@ -31,11 +32,21 @@ import type { ScrollBarRef } from '@/components/ui/ScrollBar'
31
32
  const BALAO_AGENTE =
32
33
  'prose rounded-xl bg-gray-100 px-5 py-2.5 text-sm text-gray-900 dark:bg-gray-700 dark:text-gray-100'
33
34
 
34
- const ChatView = () => {
35
+ type ChatViewProps = {
36
+ /**
37
+ * Mostra o drawer de histórico do ChatMobileNav abaixo de xl. O widget
38
+ * flutuante desliga, porque tem o próprio drawer dentro do painel.
39
+ */
40
+ showMobileNav?: boolean
41
+ }
42
+
43
+ const ChatView = ({ showMobileNav = true }: ChatViewProps) => {
35
44
  const scrollRef = useRef<ScrollBarRef>(null)
36
45
  const { selectedConversation, chatHistory, isTyping, disabledChatFresh } =
37
46
  usGenerativeChatStore()
38
47
  const { handleSend, handleApproval, handleDataSubmit } = useChatSend()
48
+ // Conversa aberta da lista: as mensagens vêm do backend na primeira vez.
49
+ const { carregando } = useConversationMessages()
39
50
 
40
51
  const scrollToBottom = () => {
41
52
  if (scrollRef.current?.getScrollElement) {
@@ -73,7 +84,7 @@ const ChatView = () => {
73
84
 
74
85
  return (
75
86
  <Card className="flex-1 h-full" bodyClass="h-full">
76
- <ChatMobileNav />
87
+ {showMobileNav && <ChatMobileNav />}
77
88
  <ChatBox
78
89
  ref={scrollRef}
79
90
  messageList={messageList}
@@ -89,10 +100,12 @@ const ChatView = () => {
89
100
  <AiThinkingIndicator />
90
101
  </div>
91
102
  )}
92
- containerClass="h-[calc(100%-30px)] xl:h-full"
103
+ containerClass={
104
+ showMobileNav ? 'h-[calc(100%-30px)] xl:h-full' : 'h-full'
105
+ }
93
106
  messageListClass="h-[calc(100%-100px)] xl:h-[calc(100%-70px)]"
94
107
  typing={
95
- isTyping
108
+ isTyping || carregando
96
109
  ? {
97
110
  id: 'ai',
98
111
  name: 'Chat AI',