@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.
- package/dist/@ecme/assets/styles/app.css +23 -3
- package/dist/@ecme/assets/styles/tailwind/index.css +25 -3
- package/dist/@ecme/components/layouts/PostLoginLayout/PostLoginLayout.js +2 -1
- package/dist/@ecme/components/layouts/PostLoginLayout/PostLoginLayout.js.map +1 -1
- package/dist/@ecme/components/template/Footer.js +1 -1
- package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiChatDrawer.js +121 -0
- package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiChatDrawer.js.map +1 -0
- package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.js +47 -18
- package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.js.map +1 -1
- package/dist/@ecme/components/template/OrbeAiWidget/useResizablePanel.js +45 -31
- package/dist/@ecme/components/template/OrbeAiWidget/useResizablePanel.js.map +1 -1
- package/dist/@ecme/components/ui/Dropdown/DropdownMenu.js +5 -1
- package/dist/@ecme/components/ui/Dropdown/DropdownMenu.js.map +1 -1
- package/dist/@ecme/index.js +4 -1
- package/dist/@ecme/services/AiService.js +22 -0
- package/dist/@ecme/services/AiService.js.map +1 -1
- package/dist/@ecme/services/index.js +4 -1
- package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistory.js +9 -23
- package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistory.js.map +1 -1
- package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistoryItem.js +6 -16
- package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistoryItem.js.map +1 -1
- package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistoryRenameDialog.js +10 -7
- package/dist/@ecme/views/concepts/ai/Chat/components/ChatHistoryRenameDialog.js.map +1 -1
- package/dist/@ecme/views/concepts/ai/Chat/components/ChatView.js +6 -4
- package/dist/@ecme/views/concepts/ai/Chat/components/ChatView.js.map +1 -1
- package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatConversations.js +83 -0
- package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatConversations.js.map +1 -0
- package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatSend.js +3 -2
- package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatSend.js.map +1 -1
- package/dist/@ecme/views/concepts/ai/Chat/hooks/useConversationMessages.js +37 -0
- package/dist/@ecme/views/concepts/ai/Chat/hooks/useConversationMessages.js.map +1 -0
- package/dist/@ecme/views/concepts/ai/Chat/store/generativeChatStore.js +25 -1
- package/dist/@ecme/views/concepts/ai/Chat/store/generativeChatStore.js.map +1 -1
- package/dist/@ecme/views/concepts/ai/Chat/utils.js +16 -1
- package/dist/@ecme/views/concepts/ai/Chat/utils.js.map +1 -1
- package/dist/@types/@ecme/components/layouts/PostLoginLayout/PostLoginLayout.d.ts.map +1 -1
- package/dist/@types/@ecme/components/template/OrbeAiWidget/OrbeAiChatDrawer.d.ts +14 -0
- package/dist/@types/@ecme/components/template/OrbeAiWidget/OrbeAiChatDrawer.d.ts.map +1 -0
- package/dist/@types/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.d.ts.map +1 -1
- package/dist/@types/@ecme/components/template/OrbeAiWidget/useResizablePanel.d.ts +12 -5
- package/dist/@types/@ecme/components/template/OrbeAiWidget/useResizablePanel.d.ts.map +1 -1
- package/dist/@types/@ecme/components/ui/Dropdown/DropdownMenu.d.ts +2 -0
- package/dist/@types/@ecme/components/ui/Dropdown/DropdownMenu.d.ts.map +1 -1
- package/dist/@types/@ecme/services/AiService.d.ts +5 -0
- package/dist/@types/@ecme/services/AiService.d.ts.map +1 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistory.d.ts +3 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistory.d.ts.map +1 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistoryItem.d.ts +1 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistoryItem.d.ts.map +1 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistoryRenameDialog.d.ts +5 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatHistoryRenameDialog.d.ts.map +1 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatView.d.ts +8 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatView.d.ts.map +1 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useChatConversations.d.ts +12 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useChatConversations.d.ts.map +1 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useChatSend.d.ts.map +1 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useConversationMessages.d.ts +12 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useConversationMessages.d.ts.map +1 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/store/generativeChatStore.d.ts +4 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/store/generativeChatStore.d.ts.map +1 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/types.d.ts +24 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/types.d.ts.map +1 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/utils.d.ts +13 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/utils.d.ts.map +1 -1
- package/dist/@types/base/configs/endpoints.config/endpoints.navigation.d.ts +8 -0
- package/dist/@types/base/configs/endpoints.config/endpoints.navigation.d.ts.map +1 -1
- package/dist/@types/base/configs/endpoints.config/transportadora/contratoFrete.d.ts +8 -0
- package/dist/@types/base/configs/endpoints.config/transportadora/contratoFrete.d.ts.map +1 -1
- package/dist/@types/base/services/modules/transportadora/ContratoFrete/ContratoFreteService.d.ts +2 -0
- package/dist/@types/base/services/modules/transportadora/ContratoFrete/ContratoFreteService.d.ts.map +1 -1
- package/dist/base/assets/styles/init.css +23 -4
- package/dist/base/configs/endpoints.config/transportadora/contratoFrete.js +3 -1
- package/dist/base/configs/endpoints.config/transportadora/contratoFrete.js.map +1 -1
- package/dist/base/index.js +3 -1
- package/dist/base/services/index.js +3 -1
- package/dist/base/services/modules/index.js +3 -1
- package/dist/base/services/modules/transportadora/ContratoFrete/ContratoFreteService.js +14 -0
- package/dist/base/services/modules/transportadora/ContratoFrete/ContratoFreteService.js.map +1 -1
- package/dist/base/services/modules/transportadora/index.js +3 -1
- package/lib/@ecme/components/layouts/PostLoginLayout/PostLoginLayout.tsx +4 -1
- package/lib/@ecme/components/template/OrbeAiWidget/OrbeAiChatDrawer.tsx +140 -0
- package/lib/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.tsx +48 -16
- package/lib/@ecme/components/template/OrbeAiWidget/useResizablePanel.ts +72 -37
- package/lib/@ecme/components/ui/Dropdown/DropdownMenu.tsx +7 -1
- package/lib/@ecme/services/AiService.ts +24 -0
- package/lib/@ecme/views/concepts/ai/Chat/components/ChatHistory.tsx +11 -27
- package/lib/@ecme/views/concepts/ai/Chat/components/ChatHistoryItem.tsx +7 -13
- package/lib/@ecme/views/concepts/ai/Chat/components/ChatHistoryRenameDialog.tsx +17 -8
- package/lib/@ecme/views/concepts/ai/Chat/components/ChatView.tsx +17 -4
- package/lib/@ecme/views/concepts/ai/Chat/hooks/useChatConversations.tsx +128 -0
- package/lib/@ecme/views/concepts/ai/Chat/hooks/useChatSend.ts +3 -2
- package/lib/@ecme/views/concepts/ai/Chat/hooks/useConversationMessages.ts +68 -0
- package/lib/@ecme/views/concepts/ai/Chat/store/generativeChatStore.ts +44 -1
- package/lib/@ecme/views/concepts/ai/Chat/types.ts +28 -0
- package/lib/@ecme/views/concepts/ai/Chat/utils.ts +33 -0
- package/lib/base/configs/endpoints.config/transportadora/contratoFrete.ts +2 -0
- package/lib/base/services/modules/transportadora/ContratoFrete/ContratoFreteService.ts +14 -0
- 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
|
|
22
|
+
const [drawerOpen, setDrawerOpen] = useState(false)
|
|
23
|
+
const { rect, startResize, anchor } = useResizablePanel()
|
|
21
24
|
|
|
22
|
-
const toggleOpen = () =>
|
|
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:
|
|
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
|
-
|
|
34
|
-
|
|
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-
|
|
64
|
-
<div className="flex items-center gap-2">
|
|
65
|
-
<
|
|
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-
|
|
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
|
-
|
|
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
|
-
/**
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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>(
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
95
|
+
start.width - dx,
|
|
66
96
|
MIN.width,
|
|
67
|
-
|
|
97
|
+
viewport.width - start.right - MARGIN,
|
|
68
98
|
)
|
|
69
99
|
}
|
|
70
|
-
if (direction.includes('
|
|
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
|
|
102
|
+
start.height - dy,
|
|
77
103
|
MIN.height,
|
|
78
|
-
|
|
104
|
+
viewport.height - start.bottom - MARGIN,
|
|
79
105
|
)
|
|
80
106
|
}
|
|
81
|
-
|
|
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
|
|
118
|
+
start.height + dy,
|
|
84
119
|
MIN.height,
|
|
85
|
-
bottom - MARGIN,
|
|
120
|
+
start.height + start.bottom - MARGIN,
|
|
86
121
|
)
|
|
87
|
-
next.
|
|
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=
|
|
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
|
|
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 = ({
|
|
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
|
-
|
|
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,
|
|
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>
|
|
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="
|
|
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">
|
|
100
|
+
<span className="text-error">Excluir</span>
|
|
107
101
|
</Dropdown.Item>
|
|
108
102
|
</Dropdown>
|
|
109
103
|
</div>
|