@orbe-agro/client-core 5.6.201 → 5.6.203
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 +82 -5
- package/dist/@ecme/assets/styles/tailwind/index.css +89 -6
- package/dist/@ecme/components/layouts/PostLoginLayout/PostLoginLayout.js +3 -0
- 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/OrbeAiWidget.js +100 -0
- package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.js.map +1 -0
- package/dist/@ecme/components/template/OrbeAiWidget/ResizeHandles.js +37 -0
- package/dist/@ecme/components/template/OrbeAiWidget/ResizeHandles.js.map +1 -0
- package/dist/@ecme/components/template/OrbeAiWidget/index.js +5 -0
- package/dist/@ecme/components/template/OrbeAiWidget/index.js.map +1 -0
- package/dist/@ecme/components/template/OrbeAiWidget/useResizablePanel.js +77 -0
- package/dist/@ecme/components/template/OrbeAiWidget/useResizablePanel.js.map +1 -0
- package/dist/@ecme/components/view/ChatBox/ChatBox.js +7 -1
- package/dist/@ecme/components/view/ChatBox/ChatBox.js.map +1 -1
- package/dist/@ecme/components/view/ChatBox/components/MessageList.js +7 -4
- package/dist/@ecme/components/view/ChatBox/components/MessageList.js.map +1 -1
- package/dist/@ecme/configs/navigation.config/concepts.navigation.config.js +0 -16
- package/dist/@ecme/configs/navigation.config/concepts.navigation.config.js.map +1 -1
- package/dist/@ecme/configs/routes.config/conceptsRoute.js +0 -9
- package/dist/@ecme/configs/routes.config/conceptsRoute.js.map +1 -1
- package/dist/@ecme/index.js +3 -1
- package/dist/@ecme/services/AiService.js +24 -4
- package/dist/@ecme/services/AiService.js.map +1 -1
- package/dist/@ecme/services/axios/AxiosBase.js +30 -24
- package/dist/@ecme/services/axios/AxiosBase.js.map +1 -1
- package/dist/@ecme/services/index.js +3 -1
- package/dist/@ecme/views/concepts/ai/Chat/components/AiThinkingIndicator.js +23 -0
- package/dist/@ecme/views/concepts/ai/Chat/components/AiThinkingIndicator.js.map +1 -0
- package/dist/@ecme/views/concepts/ai/Chat/components/ChatLandingView.js +16 -16
- package/dist/@ecme/views/concepts/ai/Chat/components/ChatLandingView.js.map +1 -1
- package/dist/@ecme/views/concepts/ai/Chat/components/ChatView.js +7 -4
- package/dist/@ecme/views/concepts/ai/Chat/components/ChatView.js.map +1 -1
- package/dist/@ecme/views/concepts/ai/Chat/components/ModelSelect.js +48 -0
- package/dist/@ecme/views/concepts/ai/Chat/components/ModelSelect.js.map +1 -0
- package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatSend.js +6 -3
- package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatSend.js.map +1 -1
- package/dist/@ecme/views/concepts/ai/Chat/store/aiModelStore.js +15 -0
- package/dist/@ecme/views/concepts/ai/Chat/store/aiModelStore.js.map +1 -0
- package/dist/@types/@ecme/components/layouts/PostLoginLayout/PostLoginLayout.d.ts.map +1 -1
- package/dist/@types/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.d.ts +3 -0
- package/dist/@types/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.d.ts.map +1 -0
- package/dist/@types/@ecme/components/template/OrbeAiWidget/ResizeHandles.d.ts +9 -0
- package/dist/@types/@ecme/components/template/OrbeAiWidget/ResizeHandles.d.ts.map +1 -0
- package/dist/@types/@ecme/components/template/OrbeAiWidget/index.d.ts +2 -0
- package/dist/@types/@ecme/components/template/OrbeAiWidget/index.d.ts.map +1 -0
- package/dist/@types/@ecme/components/template/OrbeAiWidget/useResizablePanel.d.ts +19 -0
- package/dist/@types/@ecme/components/template/OrbeAiWidget/useResizablePanel.d.ts.map +1 -0
- package/dist/@types/@ecme/components/view/ChatBox/ChatBox.d.ts.map +1 -1
- package/dist/@types/@ecme/components/view/ChatBox/components/MessageList.d.ts +3 -0
- package/dist/@types/@ecme/components/view/ChatBox/components/MessageList.d.ts.map +1 -1
- package/dist/@types/@ecme/configs/navigation.config/concepts.navigation.config.d.ts.map +1 -1
- package/dist/@types/@ecme/configs/routes.config/conceptsRoute.d.ts.map +1 -1
- package/dist/@types/@ecme/services/AiService.d.ts +12 -0
- package/dist/@types/@ecme/services/AiService.d.ts.map +1 -1
- package/dist/@types/@ecme/services/axios/AxiosBase.d.ts +12 -0
- package/dist/@types/@ecme/services/axios/AxiosBase.d.ts.map +1 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/AiThinkingIndicator.d.ts +7 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/AiThinkingIndicator.d.ts.map +1 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatView.d.ts.map +1 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/ModelSelect.d.ts +8 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/ModelSelect.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/store/aiModelStore.d.ts +25 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/store/aiModelStore.d.ts.map +1 -0
- package/dist/@types/base/components/forms/GenericForms/GenericArrayFormSection.d.ts +2 -2
- package/dist/@types/base/components/forms/GenericForms/GenericArrayFormSection.d.ts.map +1 -1
- package/dist/@types/base/configs/endpoints.config/Ticket/ticket.endpoint.config.d.ts +1 -0
- package/dist/@types/base/configs/endpoints.config/Ticket/ticket.endpoint.config.d.ts.map +1 -1
- package/dist/@types/base/configs/navigation.config/submenu/ticket/config/relatorioCarregamentoBiodiesel.submenu.d.ts +4 -0
- package/dist/@types/base/configs/navigation.config/submenu/ticket/config/relatorioCarregamentoBiodiesel.submenu.d.ts.map +1 -0
- package/dist/@types/base/configs/navigation.config/submenu/ticket/relatorios.submenu.d.ts.map +1 -1
- package/dist/@types/base/locales/lang/ptBR/modules/Ticket/ticket.nav.d.ts +1 -0
- package/dist/@types/base/locales/lang/ptBR/modules/Ticket/ticket.nav.d.ts.map +1 -1
- package/dist/@types/base/services/modules/Ticket/TicketService.d.ts +1 -0
- package/dist/@types/base/services/modules/Ticket/TicketService.d.ts.map +1 -1
- package/dist/base/assets/styles/init.css +88 -5
- package/dist/base/components/forms/GenericForms/GenericArrayFormSection.js.map +1 -1
- package/dist/base/configs/endpoints.config/Ticket/ticket.endpoint.config.js +2 -1
- package/dist/base/configs/endpoints.config/Ticket/ticket.endpoint.config.js.map +1 -1
- package/dist/base/configs/navigation.config/submenu/ticket/config/relatorioCarregamentoBiodiesel.submenu.js +16 -0
- package/dist/base/configs/navigation.config/submenu/ticket/config/relatorioCarregamentoBiodiesel.submenu.js.map +1 -0
- package/dist/base/configs/navigation.config/submenu/ticket/relatorios.submenu.js +3 -1
- package/dist/base/configs/navigation.config/submenu/ticket/relatorios.submenu.js.map +1 -1
- package/dist/base/locales/lang/ptBR/modules/Ticket/ticket.nav.js +2 -1
- package/dist/base/locales/lang/ptBR/modules/Ticket/ticket.nav.js.map +1 -1
- package/dist/base/services/modules/Ticket/TicketService.js +8 -0
- package/dist/base/services/modules/Ticket/TicketService.js.map +1 -1
- package/lib/@ecme/components/layouts/PostLoginLayout/PostLoginLayout.tsx +2 -1
- package/lib/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.tsx +125 -0
- package/lib/@ecme/components/template/OrbeAiWidget/ResizeHandles.tsx +47 -0
- package/lib/@ecme/components/template/OrbeAiWidget/index.ts +1 -0
- package/lib/@ecme/components/template/OrbeAiWidget/useResizablePanel.ts +108 -0
- package/lib/@ecme/components/view/ChatBox/ChatBox.tsx +6 -0
- package/lib/@ecme/components/view/ChatBox/components/MessageList.tsx +21 -7
- package/lib/@ecme/configs/navigation.config/concepts.navigation.config.ts +0 -16
- package/lib/@ecme/configs/routes.config/conceptsRoute.ts +0 -9
- package/lib/@ecme/services/AiService.ts +40 -0
- package/lib/@ecme/services/axios/AxiosBase.ts +45 -24
- package/lib/@ecme/views/concepts/ai/Chat/components/AiThinkingIndicator.tsx +24 -0
- package/lib/@ecme/views/concepts/ai/Chat/components/ChatLandingView.tsx +17 -17
- package/lib/@ecme/views/concepts/ai/Chat/components/ChatView.tsx +11 -4
- package/lib/@ecme/views/concepts/ai/Chat/components/ModelSelect.tsx +66 -0
- package/lib/@ecme/views/concepts/ai/Chat/hooks/useChatSend.ts +5 -3
- package/lib/@ecme/views/concepts/ai/Chat/store/aiModelStore.ts +23 -0
- package/lib/base/components/forms/GenericForms/GenericArrayFormSection.tsx +1 -1
- package/lib/base/configs/endpoints.config/Ticket/ticket.endpoint.config.ts +1 -0
- package/lib/base/configs/navigation.config/submenu/ticket/config/relatorioCarregamentoBiodiesel.submenu.ts +16 -0
- package/lib/base/configs/navigation.config/submenu/ticket/relatorios.submenu.ts +2 -0
- package/lib/base/locales/lang/ptBR/modules/Ticket/ticket.nav.ts +1 -0
- package/lib/base/services/modules/Ticket/TicketService.ts +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { AnimatePresence, motion } from 'framer-motion'
|
|
3
|
+
import {
|
|
4
|
+
TbMessageChatbot,
|
|
5
|
+
TbArrowsMaximize,
|
|
6
|
+
TbArrowsMinimize,
|
|
7
|
+
} from 'react-icons/tb'
|
|
8
|
+
import { HiX } from 'react-icons/hi'
|
|
9
|
+
import ChatView from '@/views/concepts/ai/Chat/components/ChatView'
|
|
10
|
+
import ChatHistoryRenameDialog from '@/views/concepts/ai/Chat/components/ChatHistoryRenameDialog'
|
|
11
|
+
import ModelSelect from '@/views/concepts/ai/Chat/components/ModelSelect'
|
|
12
|
+
import useResizablePanel from './useResizablePanel'
|
|
13
|
+
import ResizeHandles from './ResizeHandles'
|
|
14
|
+
|
|
15
|
+
const iconButtonClass =
|
|
16
|
+
'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
|
+
|
|
18
|
+
const OrbeAiWidget = () => {
|
|
19
|
+
const [open, setOpen] = useState(false)
|
|
20
|
+
const [fullscreen, setFullscreen] = useState(false)
|
|
21
|
+
const { rect, startResize } = useResizablePanel()
|
|
22
|
+
|
|
23
|
+
const toggleOpen = () => setOpen((prev) => !prev)
|
|
24
|
+
const toggleFullscreen = () => setFullscreen((prev) => !prev)
|
|
25
|
+
const close = () => {
|
|
26
|
+
setOpen(false)
|
|
27
|
+
setFullscreen(false)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Tela cheia: cobre o viewport inteiro (inset-0). Flutuante: usa o retângulo do resize.
|
|
31
|
+
const panelStyle = fullscreen
|
|
32
|
+
? undefined
|
|
33
|
+
: {
|
|
34
|
+
top: rect.top,
|
|
35
|
+
left: rect.left,
|
|
36
|
+
width: rect.width,
|
|
37
|
+
height: rect.height,
|
|
38
|
+
}
|
|
39
|
+
const panelVariantClass = fullscreen
|
|
40
|
+
? 'inset-0 rounded-none border-0'
|
|
41
|
+
: 'rounded-2xl border shadow-2xl'
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
className={`fixed bottom-6 right-6 flex flex-col items-end gap-3 print:hidden ${
|
|
46
|
+
fullscreen ? 'z-[9999]' : 'z-40'
|
|
47
|
+
}`}
|
|
48
|
+
>
|
|
49
|
+
<AnimatePresence>
|
|
50
|
+
{open && (
|
|
51
|
+
<motion.div
|
|
52
|
+
key="orbe-ai-panel"
|
|
53
|
+
initial={{ opacity: 0, y: 24, scale: 0.96 }}
|
|
54
|
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
55
|
+
exit={{ opacity: 0, y: 24, scale: 0.96 }}
|
|
56
|
+
transition={{ duration: 0.2, ease: 'easeOut' }}
|
|
57
|
+
style={panelStyle}
|
|
58
|
+
className={`fixed flex flex-col overflow-hidden border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800 ${panelVariantClass}`}
|
|
59
|
+
>
|
|
60
|
+
{!fullscreen && (
|
|
61
|
+
<ResizeHandles onResizeStart={startResize} />
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
<div className="flex items-center justify-between border-b border-gray-200 px-4 py-3 dark:border-gray-700">
|
|
65
|
+
<div className="flex items-center gap-2">
|
|
66
|
+
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-primary text-white">
|
|
67
|
+
<TbMessageChatbot size={20} />
|
|
68
|
+
</span>
|
|
69
|
+
<span className="font-semibold text-gray-900 dark:text-gray-100">
|
|
70
|
+
Orbe AI
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="flex items-center gap-2">
|
|
74
|
+
<ModelSelect />
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
className={iconButtonClass}
|
|
78
|
+
onClick={toggleFullscreen}
|
|
79
|
+
aria-label={
|
|
80
|
+
fullscreen
|
|
81
|
+
? 'Sair da tela cheia'
|
|
82
|
+
: 'Tela cheia'
|
|
83
|
+
}
|
|
84
|
+
>
|
|
85
|
+
{fullscreen ? (
|
|
86
|
+
<TbArrowsMinimize size={18} />
|
|
87
|
+
) : (
|
|
88
|
+
<TbArrowsMaximize size={18} />
|
|
89
|
+
)}
|
|
90
|
+
</button>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
className={iconButtonClass}
|
|
94
|
+
onClick={close}
|
|
95
|
+
aria-label="Fechar chat Orbe AI"
|
|
96
|
+
>
|
|
97
|
+
<HiX size={18} />
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div className="min-h-0 flex-1 p-2">
|
|
103
|
+
<ChatView />
|
|
104
|
+
<ChatHistoryRenameDialog />
|
|
105
|
+
</div>
|
|
106
|
+
</motion.div>
|
|
107
|
+
)}
|
|
108
|
+
</AnimatePresence>
|
|
109
|
+
|
|
110
|
+
{!fullscreen && (
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
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"
|
|
114
|
+
onClick={toggleOpen}
|
|
115
|
+
aria-label={open ? 'Fechar chat Orbe AI' : 'Abrir chat Orbe AI'}
|
|
116
|
+
aria-expanded={open}
|
|
117
|
+
>
|
|
118
|
+
{open ? <HiX size={24} /> : <TbMessageChatbot size={26} />}
|
|
119
|
+
</button>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default OrbeAiWidget
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { PointerEvent as ReactPointerEvent } from 'react'
|
|
2
|
+
import type { ResizeDirection } from './useResizablePanel'
|
|
3
|
+
|
|
4
|
+
type ResizeHandlesProps = {
|
|
5
|
+
onResizeStart: (
|
|
6
|
+
event: ReactPointerEvent,
|
|
7
|
+
direction: ResizeDirection,
|
|
8
|
+
) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const EDGES: { direction: ResizeDirection; className: string }[] = [
|
|
12
|
+
{ direction: 'top', className: 'left-0 right-0 top-0 h-1.5 cursor-ns-resize' },
|
|
13
|
+
{ direction: 'bottom', className: 'bottom-0 left-0 right-0 h-1.5 cursor-ns-resize' },
|
|
14
|
+
{ direction: 'left', className: 'bottom-0 left-0 top-0 w-1.5 cursor-ew-resize' },
|
|
15
|
+
{ direction: 'right', className: 'bottom-0 right-0 top-0 w-1.5 cursor-ew-resize' },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const CORNERS: { direction: ResizeDirection; className: string }[] = [
|
|
19
|
+
{ direction: 'top-left', className: 'left-0 top-0 cursor-nwse-resize' },
|
|
20
|
+
{ direction: 'top-right', className: 'right-0 top-0 cursor-nesw-resize' },
|
|
21
|
+
{ direction: 'bottom-left', className: 'bottom-0 left-0 cursor-nesw-resize' },
|
|
22
|
+
{ direction: 'bottom-right', className: 'bottom-0 right-0 cursor-nwse-resize' },
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
/** Alças transparentes de redimensionamento nas 4 bordas + 4 cantos do painel. */
|
|
26
|
+
const ResizeHandles = ({ onResizeStart }: ResizeHandlesProps) => (
|
|
27
|
+
<>
|
|
28
|
+
{EDGES.map(({ direction, className }) => (
|
|
29
|
+
<span
|
|
30
|
+
key={direction}
|
|
31
|
+
aria-hidden
|
|
32
|
+
onPointerDown={(event) => onResizeStart(event, direction)}
|
|
33
|
+
className={`absolute z-20 ${className}`}
|
|
34
|
+
/>
|
|
35
|
+
))}
|
|
36
|
+
{CORNERS.map(({ direction, className }) => (
|
|
37
|
+
<span
|
|
38
|
+
key={direction}
|
|
39
|
+
aria-hidden
|
|
40
|
+
onPointerDown={(event) => onResizeStart(event, direction)}
|
|
41
|
+
className={`absolute z-30 h-3 w-3 ${className}`}
|
|
42
|
+
/>
|
|
43
|
+
))}
|
|
44
|
+
</>
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
export default ResizeHandles
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './OrbeAiWidget'
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react'
|
|
2
|
+
import type { PointerEvent as ReactPointerEvent } from 'react'
|
|
3
|
+
|
|
4
|
+
export type Rect = { top: number; left: number; width: number; height: number }
|
|
5
|
+
|
|
6
|
+
export type ResizeDirection =
|
|
7
|
+
| 'top'
|
|
8
|
+
| 'bottom'
|
|
9
|
+
| 'left'
|
|
10
|
+
| 'right'
|
|
11
|
+
| 'top-left'
|
|
12
|
+
| 'top-right'
|
|
13
|
+
| 'bottom-left'
|
|
14
|
+
| 'bottom-right'
|
|
15
|
+
|
|
16
|
+
const MIN = { width: 360, height: 420 }
|
|
17
|
+
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
|
|
20
|
+
|
|
21
|
+
const clamp = (value: number, min: number, max: number) =>
|
|
22
|
+
Math.min(Math.max(value, min), max)
|
|
23
|
+
|
|
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
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
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.
|
|
40
|
+
*/
|
|
41
|
+
const useResizablePanel = () => {
|
|
42
|
+
const [rect, setRect] = useState<Rect>(getDefaultRect)
|
|
43
|
+
|
|
44
|
+
const startResize = useCallback(
|
|
45
|
+
(event: ReactPointerEvent, direction: ResizeDirection) => {
|
|
46
|
+
event.preventDefault()
|
|
47
|
+
event.stopPropagation()
|
|
48
|
+
|
|
49
|
+
const startX = event.clientX
|
|
50
|
+
const startY = event.clientY
|
|
51
|
+
const start = rect
|
|
52
|
+
const right = start.left + start.width
|
|
53
|
+
const bottom = start.top + start.height
|
|
54
|
+
|
|
55
|
+
const previousUserSelect = document.body.style.userSelect
|
|
56
|
+
document.body.style.userSelect = 'none'
|
|
57
|
+
|
|
58
|
+
const handleMove = (moveEvent: PointerEvent) => {
|
|
59
|
+
const dx = moveEvent.clientX - startX
|
|
60
|
+
const dy = moveEvent.clientY - startY
|
|
61
|
+
const next: Rect = { ...start }
|
|
62
|
+
|
|
63
|
+
if (direction.includes('right')) {
|
|
64
|
+
next.width = clamp(
|
|
65
|
+
start.width + dx,
|
|
66
|
+
MIN.width,
|
|
67
|
+
window.innerWidth - start.left - MARGIN,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
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')) {
|
|
75
|
+
next.height = clamp(
|
|
76
|
+
start.height + dy,
|
|
77
|
+
MIN.height,
|
|
78
|
+
window.innerHeight - start.top - MARGIN,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
if (direction.includes('top')) {
|
|
82
|
+
next.height = clamp(
|
|
83
|
+
start.height - dy,
|
|
84
|
+
MIN.height,
|
|
85
|
+
bottom - MARGIN,
|
|
86
|
+
)
|
|
87
|
+
next.top = bottom - next.height
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
setRect(next)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const stopResize = () => {
|
|
94
|
+
document.body.style.userSelect = previousUserSelect
|
|
95
|
+
window.removeEventListener('pointermove', handleMove)
|
|
96
|
+
window.removeEventListener('pointerup', stopResize)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
window.addEventListener('pointermove', handleMove)
|
|
100
|
+
window.addEventListener('pointerup', stopResize)
|
|
101
|
+
},
|
|
102
|
+
[rect],
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return { rect, startResize }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default useResizablePanel
|
|
@@ -32,6 +32,9 @@ const ChatBox = forwardRef<ScrollBarRef, ChatBoxProps>((props, ref) => {
|
|
|
32
32
|
customRenderer,
|
|
33
33
|
customAction,
|
|
34
34
|
bubbleClass,
|
|
35
|
+
myBubbleClass,
|
|
36
|
+
botBubbleClass,
|
|
37
|
+
renderTyping,
|
|
35
38
|
typing,
|
|
36
39
|
messageListClass,
|
|
37
40
|
containerClass,
|
|
@@ -59,6 +62,9 @@ const ChatBox = forwardRef<ScrollBarRef, ChatBoxProps>((props, ref) => {
|
|
|
59
62
|
typing={typing}
|
|
60
63
|
messageListClass={messageListClass}
|
|
61
64
|
bubbleClass={bubbleClass}
|
|
65
|
+
myBubbleClass={myBubbleClass}
|
|
66
|
+
botBubbleClass={botBubbleClass}
|
|
67
|
+
renderTyping={renderTyping}
|
|
62
68
|
/> )}
|
|
63
69
|
{children}
|
|
64
70
|
</ChatContainer>
|
|
@@ -22,6 +22,9 @@ export type MessageListProps = {
|
|
|
22
22
|
customRenderer?: (message: List, index: number) => string | ReactNode
|
|
23
23
|
customAction?: (message: List, index: number) => string | ReactNode
|
|
24
24
|
bubbleClass?: string
|
|
25
|
+
myBubbleClass?: string
|
|
26
|
+
botBubbleClass?: string
|
|
27
|
+
renderTyping?: () => ReactNode
|
|
25
28
|
messageListClass?: string
|
|
26
29
|
}
|
|
27
30
|
|
|
@@ -34,6 +37,9 @@ const MessageList = forwardRef<ScrollBarRef, MessageListProps>((props, ref) => {
|
|
|
34
37
|
customAction,
|
|
35
38
|
messageListClass,
|
|
36
39
|
bubbleClass,
|
|
40
|
+
myBubbleClass,
|
|
41
|
+
botBubbleClass,
|
|
42
|
+
renderTyping,
|
|
37
43
|
} = props
|
|
38
44
|
|
|
39
45
|
return (
|
|
@@ -49,7 +55,11 @@ const MessageList = forwardRef<ScrollBarRef, MessageListProps>((props, ref) => {
|
|
|
49
55
|
key={message.id}
|
|
50
56
|
showAvatar={showAvatar}
|
|
51
57
|
avatarGap={avatarGap}
|
|
52
|
-
bubbleClass={
|
|
58
|
+
bubbleClass={
|
|
59
|
+
message.isMyMessage
|
|
60
|
+
? myBubbleClass ?? bubbleClass
|
|
61
|
+
: botBubbleClass ?? bubbleClass
|
|
62
|
+
}
|
|
53
63
|
{...message}
|
|
54
64
|
{...(customRenderer
|
|
55
65
|
? {
|
|
@@ -71,14 +81,18 @@ const MessageList = forwardRef<ScrollBarRef, MessageListProps>((props, ref) => {
|
|
|
71
81
|
sender={typing}
|
|
72
82
|
type="regular"
|
|
73
83
|
showAvatar={showAvatar}
|
|
74
|
-
bubbleClass={bubbleClass}
|
|
84
|
+
bubbleClass={botBubbleClass ?? bubbleClass}
|
|
75
85
|
avatarGap={avatarGap}
|
|
76
86
|
content={
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<span className="
|
|
81
|
-
|
|
87
|
+
renderTyping ? (
|
|
88
|
+
renderTyping()
|
|
89
|
+
) : (
|
|
90
|
+
<span className="flex items-center gap-2">
|
|
91
|
+
<span className="size-1.5 rounded-full bg-gray-700 motion-safe:animate-[bounce_1s_ease-in-out_infinite] dark:bg-gray-300"></span>
|
|
92
|
+
<span className="size-1.5 rounded-full bg-gray-700 motion-safe:animate-[bounce_0.5s_ease-in-out_infinite] dark:bg-gray-300"></span>
|
|
93
|
+
<span className="size-1.5 rounded-full bg-gray-700 motion-safe:animate-[bounce_1s_ease-in-out_infinite] dark:bg-gray-300"></span>
|
|
94
|
+
</span>
|
|
95
|
+
)
|
|
82
96
|
}
|
|
83
97
|
/>
|
|
84
98
|
)}
|
|
@@ -38,22 +38,6 @@ const conceptsNavigationConfig: NavigationTree[] = [
|
|
|
38
38
|
},
|
|
39
39
|
},
|
|
40
40
|
subMenu: [
|
|
41
|
-
{
|
|
42
|
-
key: 'concepts.ai.chat',
|
|
43
|
-
path: `${CONCEPTS_PREFIX_PATH}/ai/chat`,
|
|
44
|
-
title: 'Chat',
|
|
45
|
-
translateKey: 'nav.conceptsAi.chat',
|
|
46
|
-
icon: 'aiChat',
|
|
47
|
-
type: NAV_ITEM_TYPE_ITEM,
|
|
48
|
-
authority: [ADMIN, USER],
|
|
49
|
-
meta: {
|
|
50
|
-
description: {
|
|
51
|
-
translateKey: 'nav.conceptsAi.chatDesc',
|
|
52
|
-
label: 'AI-powered chat systems',
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
subMenu: [],
|
|
56
|
-
},
|
|
57
41
|
{
|
|
58
42
|
key: 'concepts.ai.image',
|
|
59
43
|
path: `${CONCEPTS_PREFIX_PATH}/ai/image`,
|
|
@@ -4,15 +4,6 @@ import { ADMIN, USER } from '@/constants/roles.constant'
|
|
|
4
4
|
import type { Routes } from '@/@types/routes'
|
|
5
5
|
|
|
6
6
|
const conceptsRoute: Routes = [
|
|
7
|
-
{
|
|
8
|
-
key: 'concepts.ai.chat',
|
|
9
|
-
path: `${CONCEPTS_PREFIX_PATH}/ai/chat`,
|
|
10
|
-
component: lazy(() => import('@/views/concepts/ai/Chat')),
|
|
11
|
-
authority: [ADMIN, USER],
|
|
12
|
-
meta: {
|
|
13
|
-
pageContainerType: 'contained',
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
7
|
{
|
|
17
8
|
key: 'concepts.ai.image',
|
|
18
9
|
path: `${CONCEPTS_PREFIX_PATH}/ai/image`,
|
|
@@ -1,4 +1,31 @@
|
|
|
1
1
|
import ApiService from './ApiService'
|
|
2
|
+
import { useAiModelStore } from '@/views/concepts/ai/Chat/store/aiModelStore'
|
|
3
|
+
|
|
4
|
+
/** Informa ao backend qual modelo usar. Sem o header, ele aplica o default. */
|
|
5
|
+
export const AI_MODEL_HEADER = 'X-Ai-Model'
|
|
6
|
+
|
|
7
|
+
export type AiModel = {
|
|
8
|
+
id: string
|
|
9
|
+
label: string
|
|
10
|
+
default: boolean
|
|
11
|
+
cloud: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type GetAiModelsResponse = {
|
|
15
|
+
models: AiModel[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Header do modelo escolhido, montado por chamada.
|
|
20
|
+
*
|
|
21
|
+
* Deliberadamente NAO e um interceptor global do AxiosBase: o chat vive no
|
|
22
|
+
* layout de todos os MFEs, e um interceptor faria toda requisicao de todo
|
|
23
|
+
* modulo passar por essa checagem so para servir ao /ai/*.
|
|
24
|
+
*/
|
|
25
|
+
const aiModelHeader = () => {
|
|
26
|
+
const selectedModel = useAiModelStore.getState().selectedModel
|
|
27
|
+
return selectedModel ? { [AI_MODEL_HEADER]: selectedModel } : undefined
|
|
28
|
+
}
|
|
2
29
|
|
|
3
30
|
export async function apiPostChat<T>(data: {
|
|
4
31
|
prompt: string
|
|
@@ -8,6 +35,8 @@ export async function apiPostChat<T>(data: {
|
|
|
8
35
|
url: '/ai/chat',
|
|
9
36
|
method: 'post',
|
|
10
37
|
data,
|
|
38
|
+
headers: aiModelHeader(),
|
|
39
|
+
skipGlobalLoading: true,
|
|
11
40
|
})
|
|
12
41
|
}
|
|
13
42
|
|
|
@@ -15,6 +44,15 @@ export async function apiGetChatHistory<T>() {
|
|
|
15
44
|
return ApiService.fetchDataWithAxios<T>({
|
|
16
45
|
url: '/ai/chat/history',
|
|
17
46
|
method: 'get',
|
|
47
|
+
skipGlobalLoading: true,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function apiGetAiModels<T>() {
|
|
52
|
+
return ApiService.fetchDataWithAxios<T>({
|
|
53
|
+
url: '/ai/models',
|
|
54
|
+
method: 'get',
|
|
55
|
+
skipGlobalLoading: true,
|
|
18
56
|
})
|
|
19
57
|
}
|
|
20
58
|
|
|
@@ -25,6 +63,7 @@ export async function apiGetImages<T, U extends Record<string, unknown>>(
|
|
|
25
63
|
url: '/ai/images',
|
|
26
64
|
method: 'get',
|
|
27
65
|
params,
|
|
66
|
+
skipGlobalLoading: true,
|
|
28
67
|
})
|
|
29
68
|
}
|
|
30
69
|
|
|
@@ -35,5 +74,6 @@ export async function apiPostImages<T, U extends Record<string, unknown>>(
|
|
|
35
74
|
url: '/ai/images',
|
|
36
75
|
method: 'post',
|
|
37
76
|
data,
|
|
77
|
+
skipGlobalLoading: true,
|
|
38
78
|
})
|
|
39
79
|
}
|
|
@@ -6,6 +6,19 @@ import { useLoadingConfigStore, getCurrentMicrofrontendId } from "@base/store";
|
|
|
6
6
|
import AxiosRequestIntrceptorConfigCallback from "./AxiosRequestIntrceptorConfigCallback";
|
|
7
7
|
import AxiosResponseIntrceptorErrorCallback from "./AxiosResponseIntrceptorErrorCallback";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Permite que uma requisição específica seja excluída do loading global
|
|
11
|
+
* (overlay de tela cheia). Útil para chamadas que exibem seu próprio
|
|
12
|
+
* indicador de carregamento — ex.: o chat de IA, que mostra o balãozinho
|
|
13
|
+
* de "digitando". Comportamento padrão inalterado: só afeta requisições
|
|
14
|
+
* que marcarem explicitamente `skipGlobalLoading: true`.
|
|
15
|
+
*/
|
|
16
|
+
declare module "axios" {
|
|
17
|
+
interface AxiosRequestConfig {
|
|
18
|
+
skipGlobalLoading?: boolean;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
const AxiosBase = axios.create({
|
|
10
23
|
timeout: 600000,
|
|
11
24
|
baseURL: API_URL,
|
|
@@ -13,25 +26,37 @@ const AxiosBase = axios.create({
|
|
|
13
26
|
|
|
14
27
|
let activeRequests = 0;
|
|
15
28
|
|
|
29
|
+
const startGlobalLoading = () => {
|
|
30
|
+
const microfrontendId = getCurrentMicrofrontendId();
|
|
31
|
+
const loadingConfig = useLoadingConfigStore.getState();
|
|
32
|
+
if (loadingConfig.getGlobalLoadingEnabled(microfrontendId)) {
|
|
33
|
+
useThemeStore.getState().setLoading(true);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const stopGlobalLoading = () => {
|
|
38
|
+
const microfrontendId = getCurrentMicrofrontendId();
|
|
39
|
+
const loadingConfig = useLoadingConfigStore.getState();
|
|
40
|
+
if (loadingConfig.getGlobalLoadingEnabled(microfrontendId)) {
|
|
41
|
+
useThemeStore.getState().setLoading(false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
16
45
|
AxiosBase.interceptors.request.use(
|
|
17
46
|
(config) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (loadingConfig.getGlobalLoadingEnabled(microfrontendId)) {
|
|
23
|
-
useThemeStore.getState().setLoading(true);
|
|
47
|
+
if (!config.skipGlobalLoading) {
|
|
48
|
+
activeRequests++;
|
|
49
|
+
if (activeRequests === 1) {
|
|
50
|
+
startGlobalLoading();
|
|
24
51
|
}
|
|
25
52
|
}
|
|
26
53
|
return AxiosRequestIntrceptorConfigCallback(config);
|
|
27
54
|
},
|
|
28
55
|
(error) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (loadingConfig.getGlobalLoadingEnabled(microfrontendId)) {
|
|
34
|
-
useThemeStore.getState().setLoading(false);
|
|
56
|
+
if (!error?.config?.skipGlobalLoading) {
|
|
57
|
+
activeRequests = Math.max(0, activeRequests - 1);
|
|
58
|
+
if (activeRequests === 0) {
|
|
59
|
+
stopGlobalLoading();
|
|
35
60
|
}
|
|
36
61
|
}
|
|
37
62
|
return Promise.reject(error);
|
|
@@ -40,23 +65,19 @@ AxiosBase.interceptors.request.use(
|
|
|
40
65
|
|
|
41
66
|
AxiosBase.interceptors.response.use(
|
|
42
67
|
(response) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (loadingConfig.getGlobalLoadingEnabled(microfrontendId)) {
|
|
48
|
-
useThemeStore.getState().setLoading(false);
|
|
68
|
+
if (!response.config?.skipGlobalLoading) {
|
|
69
|
+
activeRequests = Math.max(0, activeRequests - 1);
|
|
70
|
+
if (activeRequests === 0) {
|
|
71
|
+
stopGlobalLoading();
|
|
49
72
|
}
|
|
50
73
|
}
|
|
51
74
|
return response;
|
|
52
75
|
},
|
|
53
76
|
(error: AxiosError) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (loadingConfig.getGlobalLoadingEnabled(microfrontendId)) {
|
|
59
|
-
useThemeStore.getState().setLoading(false);
|
|
77
|
+
if (!error.config?.skipGlobalLoading) {
|
|
78
|
+
activeRequests = Math.max(0, activeRequests - 1);
|
|
79
|
+
if (activeRequests === 0) {
|
|
80
|
+
stopGlobalLoading();
|
|
60
81
|
}
|
|
61
82
|
}
|
|
62
83
|
AxiosResponseIntrceptorErrorCallback(error);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { TbSparkles } from 'react-icons/tb'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Indicador de "pensando" do agente Orbe AI — loader profissional exibido
|
|
5
|
+
* enquanto a resposta está sendo gerada (estado isTyping do chat).
|
|
6
|
+
*/
|
|
7
|
+
const AiThinkingIndicator = () => {
|
|
8
|
+
return (
|
|
9
|
+
<span className="flex items-center gap-2 text-gray-500 dark:text-gray-400">
|
|
10
|
+
<TbSparkles
|
|
11
|
+
size={18}
|
|
12
|
+
className="text-primary motion-safe:animate-pulse"
|
|
13
|
+
/>
|
|
14
|
+
<span className="text-sm">Orbe AI está pensando</span>
|
|
15
|
+
<span className="inline-flex items-center gap-1">
|
|
16
|
+
<span className="size-1.5 rounded-full bg-primary motion-safe:animate-bounce [animation-delay:-0.3s]" />
|
|
17
|
+
<span className="size-1.5 rounded-full bg-primary motion-safe:animate-bounce [animation-delay:-0.15s]" />
|
|
18
|
+
<span className="size-1.5 rounded-full bg-primary motion-safe:animate-bounce" />
|
|
19
|
+
</span>
|
|
20
|
+
</span>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default AiThinkingIndicator
|
|
@@ -22,23 +22,23 @@ const promptSuggestion: {
|
|
|
22
22
|
type: PromptType
|
|
23
23
|
}[] = [
|
|
24
24
|
{
|
|
25
|
-
title: '
|
|
26
|
-
prompt:
|
|
25
|
+
title: 'Como consultar um pedido de venda?',
|
|
26
|
+
prompt: 'Como faço para consultar um pedido de venda no Orbe Agro?',
|
|
27
27
|
type: 'guide',
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
|
-
title: '
|
|
31
|
-
prompt:
|
|
32
|
-
type: '
|
|
30
|
+
title: 'Para que serve o módulo de Trade?',
|
|
31
|
+
prompt: 'Explique para que serve o módulo de Trade no Orbe Agro.',
|
|
32
|
+
type: 'idea',
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
|
-
title: '
|
|
36
|
-
prompt:
|
|
37
|
-
type: '
|
|
35
|
+
title: 'Como abrir um processo de armazenagem?',
|
|
36
|
+
prompt: 'Quais são os passos para abrir um processo de armazenagem de grãos no Orbe Agro?',
|
|
37
|
+
type: 'writing',
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
|
-
title: '
|
|
41
|
-
prompt:
|
|
40
|
+
title: 'Onde vejo o limite de crédito de um cliente?',
|
|
41
|
+
prompt: 'Onde encontro o limite de crédito de um cliente no Orbe Agro?',
|
|
42
42
|
type: 'coding',
|
|
43
43
|
},
|
|
44
44
|
]
|
|
@@ -47,20 +47,20 @@ const ChatLandingView = () => {
|
|
|
47
47
|
const { handleSend } = useChatSend()
|
|
48
48
|
|
|
49
49
|
return (
|
|
50
|
-
<div className="max-w-[900px] w-full mx-auto mt-
|
|
50
|
+
<div className="max-w-[900px] w-full mx-auto mt-6">
|
|
51
51
|
<div>
|
|
52
|
-
<div className="heading-text text-
|
|
53
|
-
<span className="font-semibold bg-gradient-to-r from-indigo-500 to-red-400 bg-clip-text text-transparent text-
|
|
54
|
-
|
|
52
|
+
<div className="heading-text text-2xl leading-snug">
|
|
53
|
+
<span className="font-semibold bg-gradient-to-r from-indigo-500 to-red-400 bg-clip-text text-transparent text-3xl">
|
|
54
|
+
Olá!
|
|
55
55
|
</span>
|
|
56
56
|
<br />
|
|
57
|
-
<span>
|
|
57
|
+
<span>Como posso te ajudar hoje?</span>
|
|
58
58
|
</div>
|
|
59
|
-
<div className="mt-
|
|
59
|
+
<div className="mt-6 grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
60
60
|
{promptSuggestion.map((suggestion) => (
|
|
61
61
|
<div
|
|
62
62
|
key={suggestion.title}
|
|
63
|
-
className="flex flex-col gap-
|
|
63
|
+
className="flex flex-col gap-3 justify-between rounded-xl bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 p-4 min-h-28 cursor-pointer"
|
|
64
64
|
role="button"
|
|
65
65
|
onClick={() => handleSend(suggestion.title)}
|
|
66
66
|
>
|