@orbe-agro/client-core 5.6.259 → 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 (98) 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/layouts/PostLoginLayout/PostLoginLayout.js +2 -1
  4. package/dist/@ecme/components/layouts/PostLoginLayout/PostLoginLayout.js.map +1 -1
  5. package/dist/@ecme/components/template/Footer.js +1 -1
  6. package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiChatDrawer.js +121 -0
  7. package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiChatDrawer.js.map +1 -0
  8. package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.js +47 -18
  9. package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.js.map +1 -1
  10. package/dist/@ecme/components/template/OrbeAiWidget/useResizablePanel.js +45 -31
  11. package/dist/@ecme/components/template/OrbeAiWidget/useResizablePanel.js.map +1 -1
  12. package/dist/@ecme/components/ui/Dropdown/DropdownMenu.js +5 -1
  13. package/dist/@ecme/components/ui/Dropdown/DropdownMenu.js.map +1 -1
  14. package/dist/@ecme/index.js +4 -1
  15. package/dist/@ecme/services/AiService.js +22 -0
  16. package/dist/@ecme/services/AiService.js.map +1 -1
  17. package/dist/@ecme/services/index.js +4 -1
  18. package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistory.js +9 -23
  19. package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistory.js.map +1 -1
  20. package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistoryItem.js +6 -16
  21. package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistoryItem.js.map +1 -1
  22. package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistoryRenameDialog.js +10 -7
  23. package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistoryRenameDialog.js.map +1 -1
  24. package/dist/@ecme/views/concepts/ai/Chat/components/ChatView.js +6 -4
  25. package/dist/@ecme/views/concepts/ai/Chat/components/ChatView.js.map +1 -1
  26. package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatConversations.js +83 -0
  27. package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatConversations.js.map +1 -0
  28. package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatSend.js +3 -2
  29. package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatSend.js.map +1 -1
  30. package/dist/@ecme/views/concepts/ai/Chat/hooks/useConversationMessages.js +37 -0
  31. package/dist/@ecme/views/concepts/ai/Chat/hooks/useConversationMessages.js.map +1 -0
  32. package/dist/@ecme/views/concepts/ai/Chat/store/generativeChatStore.js +25 -1
  33. package/dist/@ecme/views/concepts/ai/Chat/store/generativeChatStore.js.map +1 -1
  34. package/dist/@ecme/views/concepts/ai/Chat/utils.js +16 -1
  35. package/dist/@ecme/views/concepts/ai/Chat/utils.js.map +1 -1
  36. package/dist/@types/@ecme/components/layouts/PostLoginLayout/PostLoginLayout.d.ts.map +1 -1
  37. package/dist/@types/@ecme/components/template/OrbeAiWidget/OrbeAiChatDrawer.d.ts +14 -0
  38. package/dist/@types/@ecme/components/template/OrbeAiWidget/OrbeAiChatDrawer.d.ts.map +1 -0
  39. package/dist/@types/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.d.ts.map +1 -1
  40. package/dist/@types/@ecme/components/template/OrbeAiWidget/useResizablePanel.d.ts +12 -5
  41. package/dist/@types/@ecme/components/template/OrbeAiWidget/useResizablePanel.d.ts.map +1 -1
  42. package/dist/@types/@ecme/components/ui/Dropdown/DropdownMenu.d.ts +2 -0
  43. package/dist/@types/@ecme/components/ui/Dropdown/DropdownMenu.d.ts.map +1 -1
  44. package/dist/@types/@ecme/services/AiService.d.ts +5 -0
  45. package/dist/@types/@ecme/services/AiService.d.ts.map +1 -1
  46. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistory.d.ts +3 -1
  47. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistory.d.ts.map +1 -1
  48. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistoryItem.d.ts +1 -1
  49. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistoryItem.d.ts.map +1 -1
  50. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistoryRenameDialog.d.ts +5 -1
  51. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistoryRenameDialog.d.ts.map +1 -1
  52. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatView.d.ts +8 -1
  53. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatView.d.ts.map +1 -1
  54. package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useChatConversations.d.ts +12 -0
  55. package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useChatConversations.d.ts.map +1 -0
  56. package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useChatSend.d.ts.map +1 -1
  57. package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useConversationMessages.d.ts +12 -0
  58. package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useConversationMessages.d.ts.map +1 -0
  59. package/dist/@types/@ecme/views/concepts/ai/Chat/store/generativeChatStore.d.ts +4 -0
  60. package/dist/@types/@ecme/views/concepts/ai/Chat/store/generativeChatStore.d.ts.map +1 -1
  61. package/dist/@types/@ecme/views/concepts/ai/Chat/types.d.ts +24 -0
  62. package/dist/@types/@ecme/views/concepts/ai/Chat/types.d.ts.map +1 -1
  63. package/dist/@types/@ecme/views/concepts/ai/Chat/utils.d.ts +13 -0
  64. package/dist/@types/@ecme/views/concepts/ai/Chat/utils.d.ts.map +1 -1
  65. package/dist/@types/base/configs/endpoints.config/endpoints.navigation.d.ts +8 -0
  66. package/dist/@types/base/configs/endpoints.config/endpoints.navigation.d.ts.map +1 -1
  67. package/dist/@types/base/configs/endpoints.config/transportadora/contratoFrete.d.ts +8 -0
  68. package/dist/@types/base/configs/endpoints.config/transportadora/contratoFrete.d.ts.map +1 -1
  69. package/dist/@types/base/services/modules/transportadora/ContratoFrete/ContratoFreteService.d.ts +2 -0
  70. package/dist/@types/base/services/modules/transportadora/ContratoFrete/ContratoFreteService.d.ts.map +1 -1
  71. package/dist/base/assets/styles/init.css +23 -4
  72. package/dist/base/configs/endpoints.config/transportadora/contratoFrete.js +3 -1
  73. package/dist/base/configs/endpoints.config/transportadora/contratoFrete.js.map +1 -1
  74. package/dist/base/index.js +3 -1
  75. package/dist/base/services/index.js +3 -1
  76. package/dist/base/services/modules/index.js +3 -1
  77. package/dist/base/services/modules/transportadora/ContratoFrete/ContratoFreteService.js +14 -0
  78. package/dist/base/services/modules/transportadora/ContratoFrete/ContratoFreteService.js.map +1 -1
  79. package/dist/base/services/modules/transportadora/index.js +3 -1
  80. package/lib/@ecme/components/layouts/PostLoginLayout/PostLoginLayout.tsx +4 -1
  81. package/lib/@ecme/components/template/OrbeAiWidget/OrbeAiChatDrawer.tsx +140 -0
  82. package/lib/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.tsx +48 -16
  83. package/lib/@ecme/components/template/OrbeAiWidget/useResizablePanel.ts +72 -37
  84. package/lib/@ecme/components/ui/Dropdown/DropdownMenu.tsx +7 -1
  85. package/lib/@ecme/services/AiService.ts +24 -0
  86. package/lib/@ecme/views/concepts/ai/Chat/components/ChatHistory.tsx +11 -27
  87. package/lib/@ecme/views/concepts/ai/Chat/components/ChatHistoryItem.tsx +7 -13
  88. package/lib/@ecme/views/concepts/ai/Chat/components/ChatHistoryRenameDialog.tsx +17 -8
  89. package/lib/@ecme/views/concepts/ai/Chat/components/ChatView.tsx +17 -4
  90. package/lib/@ecme/views/concepts/ai/Chat/hooks/useChatConversations.tsx +128 -0
  91. package/lib/@ecme/views/concepts/ai/Chat/hooks/useChatSend.ts +3 -2
  92. package/lib/@ecme/views/concepts/ai/Chat/hooks/useConversationMessages.ts +68 -0
  93. package/lib/@ecme/views/concepts/ai/Chat/store/generativeChatStore.ts +44 -1
  94. package/lib/@ecme/views/concepts/ai/Chat/types.ts +28 -0
  95. package/lib/@ecme/views/concepts/ai/Chat/utils.ts +33 -0
  96. package/lib/base/configs/endpoints.config/transportadora/contratoFrete.ts +2 -0
  97. package/lib/base/services/modules/transportadora/ContratoFrete/ContratoFreteService.ts +14 -0
  98. package/package.json +1 -1
@@ -0,0 +1,140 @@
1
+ import { useEffect, useMemo, useState } from 'react'
2
+ import debounce from 'lodash/debounce'
3
+ import { AnimatePresence, motion } from 'framer-motion'
4
+ import { TbPlus, TbSearch } from 'react-icons/tb'
5
+ import { HiX } from 'react-icons/hi'
6
+ import Button from '@/components/ui/Button'
7
+ import ChatHistory from '@/views/concepts/ai/Chat/components/ChatHistory'
8
+ import { usGenerativeChatStore } from '@/views/concepts/ai/Chat/store/generativeChatStore'
9
+ import type { ChangeEvent } from 'react'
10
+
11
+ type OrbeAiChatDrawerProps = {
12
+ open: boolean
13
+ onClose: () => void
14
+ }
15
+
16
+ const iconButtonClass =
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'
18
+
19
+ /**
20
+ * Drawer de conversas DENTRO do painel do widget.
21
+ *
22
+ * Não usa o Drawer do ui: ele é portalizado no body com overlay em z-30 e
23
+ * abriria atrás do widget (z-40 / z-[9999]), cobrindo a página inteira. Aqui é
24
+ * `absolute`, então fica recortado pelo painel em qualquer tamanho.
25
+ */
26
+ const OrbeAiChatDrawer = ({ open, onClose }: OrbeAiChatDrawerProps) => {
27
+ const [queryText, setQueryText] = useState('')
28
+ const { setSelectedConversation } = usGenerativeChatStore()
29
+
30
+ // Memoizado: o store re-renderiza o drawer a cada token do streaming, e um
31
+ // debounce recriado a cada render perderia a busca pendente.
32
+ const debounceFn = useMemo(
33
+ () =>
34
+ debounce((e: ChangeEvent<HTMLInputElement>) => {
35
+ setQueryText(e.target.value)
36
+ }, 500),
37
+ [],
38
+ )
39
+
40
+ useEffect(() => () => debounceFn.cancel(), [debounceFn])
41
+
42
+ // O input desmonta ao fechar e volta vazio; o filtro precisa acompanhar.
43
+ useEffect(() => {
44
+ if (!open) {
45
+ debounceFn.cancel()
46
+ setQueryText('')
47
+ }
48
+ }, [open, debounceFn])
49
+
50
+ useEffect(() => {
51
+ if (!open) return
52
+ const handleKeyDown = (event: KeyboardEvent) => {
53
+ if (event.key === 'Escape') onClose()
54
+ }
55
+ window.addEventListener('keydown', handleKeyDown)
56
+ return () => window.removeEventListener('keydown', handleKeyDown)
57
+ }, [open, onClose])
58
+
59
+ const handleNewChat = () => {
60
+ setSelectedConversation('')
61
+ onClose()
62
+ }
63
+
64
+ return (
65
+ <AnimatePresence>
66
+ {open && (
67
+ <>
68
+ <motion.div
69
+ key="orbe-ai-drawer-backdrop"
70
+ aria-hidden
71
+ initial={{ opacity: 0 }}
72
+ animate={{ opacity: 1 }}
73
+ exit={{ opacity: 0 }}
74
+ transition={{ duration: 0.2 }}
75
+ className="absolute inset-0 z-40 bg-black/40"
76
+ onClick={onClose}
77
+ />
78
+ <motion.aside
79
+ key="orbe-ai-drawer"
80
+ role="dialog"
81
+ aria-modal="true"
82
+ aria-label="Conversas"
83
+ initial={{ x: '-100%' }}
84
+ animate={{ x: 0 }}
85
+ exit={{ x: '-100%' }}
86
+ transition={{ duration: 0.2, ease: 'easeOut' }}
87
+ className="absolute inset-y-0 left-0 z-50 flex w-[85%] max-w-[300px] flex-col border-r border-gray-200 bg-white shadow-xl dark:border-gray-700 dark:bg-gray-800"
88
+ >
89
+ <div className="flex items-center justify-between border-b border-gray-200 px-4 py-3 dark:border-gray-700">
90
+ <span className="font-semibold text-gray-900 dark:text-gray-100">
91
+ Conversas
92
+ </span>
93
+ <button
94
+ type="button"
95
+ className={iconButtonClass}
96
+ onClick={onClose}
97
+ aria-label="Fechar conversas"
98
+ >
99
+ <HiX size={18} />
100
+ </button>
101
+ </div>
102
+
103
+ <div className="flex flex-col gap-3 px-4 pt-4">
104
+ <Button
105
+ block
106
+ variant="solid"
107
+ size="sm"
108
+ icon={<TbPlus />}
109
+ onClick={handleNewChat}
110
+ >
111
+ Novo chat
112
+ </Button>
113
+ <label className="flex h-9 items-center gap-2 rounded-lg border border-gray-200 px-3 focus-within:border-primary dark:border-gray-600">
114
+ <TbSearch className="shrink-0 text-lg text-gray-400" />
115
+ <input
116
+ className="h-full min-w-0 flex-1 bg-transparent text-sm placeholder:text-gray-400 focus:outline-none heading-text"
117
+ placeholder="Buscar conversa"
118
+ onChange={debounceFn}
119
+ />
120
+ </label>
121
+ </div>
122
+
123
+ <div className="px-4 pb-1 pt-4 text-xs font-semibold uppercase tracking-wide text-gray-400">
124
+ Histórico
125
+ </div>
126
+ <div className="min-h-0 flex-1">
127
+ <ChatHistory
128
+ queryText={queryText}
129
+ onClick={onClose}
130
+ dropdownFloatingClass="!z-[10000]"
131
+ />
132
+ </div>
133
+ </motion.aside>
134
+ </>
135
+ )}
136
+ </AnimatePresence>
137
+ )
138
+ }
139
+
140
+ export default OrbeAiChatDrawer
@@ -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>