@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.
Files changed (112) hide show
  1. package/dist/@ecme/assets/styles/app.css +82 -5
  2. package/dist/@ecme/assets/styles/tailwind/index.css +89 -6
  3. package/dist/@ecme/components/layouts/PostLoginLayout/PostLoginLayout.js +3 -0
  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/OrbeAiWidget.js +100 -0
  7. package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.js.map +1 -0
  8. package/dist/@ecme/components/template/OrbeAiWidget/ResizeHandles.js +37 -0
  9. package/dist/@ecme/components/template/OrbeAiWidget/ResizeHandles.js.map +1 -0
  10. package/dist/@ecme/components/template/OrbeAiWidget/index.js +5 -0
  11. package/dist/@ecme/components/template/OrbeAiWidget/index.js.map +1 -0
  12. package/dist/@ecme/components/template/OrbeAiWidget/useResizablePanel.js +77 -0
  13. package/dist/@ecme/components/template/OrbeAiWidget/useResizablePanel.js.map +1 -0
  14. package/dist/@ecme/components/view/ChatBox/ChatBox.js +7 -1
  15. package/dist/@ecme/components/view/ChatBox/ChatBox.js.map +1 -1
  16. package/dist/@ecme/components/view/ChatBox/components/MessageList.js +7 -4
  17. package/dist/@ecme/components/view/ChatBox/components/MessageList.js.map +1 -1
  18. package/dist/@ecme/configs/navigation.config/concepts.navigation.config.js +0 -16
  19. package/dist/@ecme/configs/navigation.config/concepts.navigation.config.js.map +1 -1
  20. package/dist/@ecme/configs/routes.config/conceptsRoute.js +0 -9
  21. package/dist/@ecme/configs/routes.config/conceptsRoute.js.map +1 -1
  22. package/dist/@ecme/index.js +3 -1
  23. package/dist/@ecme/services/AiService.js +24 -4
  24. package/dist/@ecme/services/AiService.js.map +1 -1
  25. package/dist/@ecme/services/axios/AxiosBase.js +30 -24
  26. package/dist/@ecme/services/axios/AxiosBase.js.map +1 -1
  27. package/dist/@ecme/services/index.js +3 -1
  28. package/dist/@ecme/views/concepts/ai/Chat/components/AiThinkingIndicator.js +23 -0
  29. package/dist/@ecme/views/concepts/ai/Chat/components/AiThinkingIndicator.js.map +1 -0
  30. package/dist/@ecme/views/concepts/ai/Chat/components/ChatLandingView.js +16 -16
  31. package/dist/@ecme/views/concepts/ai/Chat/components/ChatLandingView.js.map +1 -1
  32. package/dist/@ecme/views/concepts/ai/Chat/components/ChatView.js +7 -4
  33. package/dist/@ecme/views/concepts/ai/Chat/components/ChatView.js.map +1 -1
  34. package/dist/@ecme/views/concepts/ai/Chat/components/ModelSelect.js +48 -0
  35. package/dist/@ecme/views/concepts/ai/Chat/components/ModelSelect.js.map +1 -0
  36. package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatSend.js +6 -3
  37. package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatSend.js.map +1 -1
  38. package/dist/@ecme/views/concepts/ai/Chat/store/aiModelStore.js +15 -0
  39. package/dist/@ecme/views/concepts/ai/Chat/store/aiModelStore.js.map +1 -0
  40. package/dist/@types/@ecme/components/layouts/PostLoginLayout/PostLoginLayout.d.ts.map +1 -1
  41. package/dist/@types/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.d.ts +3 -0
  42. package/dist/@types/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.d.ts.map +1 -0
  43. package/dist/@types/@ecme/components/template/OrbeAiWidget/ResizeHandles.d.ts +9 -0
  44. package/dist/@types/@ecme/components/template/OrbeAiWidget/ResizeHandles.d.ts.map +1 -0
  45. package/dist/@types/@ecme/components/template/OrbeAiWidget/index.d.ts +2 -0
  46. package/dist/@types/@ecme/components/template/OrbeAiWidget/index.d.ts.map +1 -0
  47. package/dist/@types/@ecme/components/template/OrbeAiWidget/useResizablePanel.d.ts +19 -0
  48. package/dist/@types/@ecme/components/template/OrbeAiWidget/useResizablePanel.d.ts.map +1 -0
  49. package/dist/@types/@ecme/components/view/ChatBox/ChatBox.d.ts.map +1 -1
  50. package/dist/@types/@ecme/components/view/ChatBox/components/MessageList.d.ts +3 -0
  51. package/dist/@types/@ecme/components/view/ChatBox/components/MessageList.d.ts.map +1 -1
  52. package/dist/@types/@ecme/configs/navigation.config/concepts.navigation.config.d.ts.map +1 -1
  53. package/dist/@types/@ecme/configs/routes.config/conceptsRoute.d.ts.map +1 -1
  54. package/dist/@types/@ecme/services/AiService.d.ts +12 -0
  55. package/dist/@types/@ecme/services/AiService.d.ts.map +1 -1
  56. package/dist/@types/@ecme/services/axios/AxiosBase.d.ts +12 -0
  57. package/dist/@types/@ecme/services/axios/AxiosBase.d.ts.map +1 -1
  58. package/dist/@types/@ecme/views/concepts/ai/Chat/components/AiThinkingIndicator.d.ts +7 -0
  59. package/dist/@types/@ecme/views/concepts/ai/Chat/components/AiThinkingIndicator.d.ts.map +1 -0
  60. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatView.d.ts.map +1 -1
  61. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ModelSelect.d.ts +8 -0
  62. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ModelSelect.d.ts.map +1 -0
  63. package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useChatSend.d.ts.map +1 -1
  64. package/dist/@types/@ecme/views/concepts/ai/Chat/store/aiModelStore.d.ts +25 -0
  65. package/dist/@types/@ecme/views/concepts/ai/Chat/store/aiModelStore.d.ts.map +1 -0
  66. package/dist/@types/base/components/forms/GenericForms/GenericArrayFormSection.d.ts +2 -2
  67. package/dist/@types/base/components/forms/GenericForms/GenericArrayFormSection.d.ts.map +1 -1
  68. package/dist/@types/base/configs/endpoints.config/Ticket/ticket.endpoint.config.d.ts +1 -0
  69. package/dist/@types/base/configs/endpoints.config/Ticket/ticket.endpoint.config.d.ts.map +1 -1
  70. package/dist/@types/base/configs/navigation.config/submenu/ticket/config/relatorioCarregamentoBiodiesel.submenu.d.ts +4 -0
  71. package/dist/@types/base/configs/navigation.config/submenu/ticket/config/relatorioCarregamentoBiodiesel.submenu.d.ts.map +1 -0
  72. package/dist/@types/base/configs/navigation.config/submenu/ticket/relatorios.submenu.d.ts.map +1 -1
  73. package/dist/@types/base/locales/lang/ptBR/modules/Ticket/ticket.nav.d.ts +1 -0
  74. package/dist/@types/base/locales/lang/ptBR/modules/Ticket/ticket.nav.d.ts.map +1 -1
  75. package/dist/@types/base/services/modules/Ticket/TicketService.d.ts +1 -0
  76. package/dist/@types/base/services/modules/Ticket/TicketService.d.ts.map +1 -1
  77. package/dist/base/assets/styles/init.css +88 -5
  78. package/dist/base/components/forms/GenericForms/GenericArrayFormSection.js.map +1 -1
  79. package/dist/base/configs/endpoints.config/Ticket/ticket.endpoint.config.js +2 -1
  80. package/dist/base/configs/endpoints.config/Ticket/ticket.endpoint.config.js.map +1 -1
  81. package/dist/base/configs/navigation.config/submenu/ticket/config/relatorioCarregamentoBiodiesel.submenu.js +16 -0
  82. package/dist/base/configs/navigation.config/submenu/ticket/config/relatorioCarregamentoBiodiesel.submenu.js.map +1 -0
  83. package/dist/base/configs/navigation.config/submenu/ticket/relatorios.submenu.js +3 -1
  84. package/dist/base/configs/navigation.config/submenu/ticket/relatorios.submenu.js.map +1 -1
  85. package/dist/base/locales/lang/ptBR/modules/Ticket/ticket.nav.js +2 -1
  86. package/dist/base/locales/lang/ptBR/modules/Ticket/ticket.nav.js.map +1 -1
  87. package/dist/base/services/modules/Ticket/TicketService.js +8 -0
  88. package/dist/base/services/modules/Ticket/TicketService.js.map +1 -1
  89. package/lib/@ecme/components/layouts/PostLoginLayout/PostLoginLayout.tsx +2 -1
  90. package/lib/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.tsx +125 -0
  91. package/lib/@ecme/components/template/OrbeAiWidget/ResizeHandles.tsx +47 -0
  92. package/lib/@ecme/components/template/OrbeAiWidget/index.ts +1 -0
  93. package/lib/@ecme/components/template/OrbeAiWidget/useResizablePanel.ts +108 -0
  94. package/lib/@ecme/components/view/ChatBox/ChatBox.tsx +6 -0
  95. package/lib/@ecme/components/view/ChatBox/components/MessageList.tsx +21 -7
  96. package/lib/@ecme/configs/navigation.config/concepts.navigation.config.ts +0 -16
  97. package/lib/@ecme/configs/routes.config/conceptsRoute.ts +0 -9
  98. package/lib/@ecme/services/AiService.ts +40 -0
  99. package/lib/@ecme/services/axios/AxiosBase.ts +45 -24
  100. package/lib/@ecme/views/concepts/ai/Chat/components/AiThinkingIndicator.tsx +24 -0
  101. package/lib/@ecme/views/concepts/ai/Chat/components/ChatLandingView.tsx +17 -17
  102. package/lib/@ecme/views/concepts/ai/Chat/components/ChatView.tsx +11 -4
  103. package/lib/@ecme/views/concepts/ai/Chat/components/ModelSelect.tsx +66 -0
  104. package/lib/@ecme/views/concepts/ai/Chat/hooks/useChatSend.ts +5 -3
  105. package/lib/@ecme/views/concepts/ai/Chat/store/aiModelStore.ts +23 -0
  106. package/lib/base/components/forms/GenericForms/GenericArrayFormSection.tsx +1 -1
  107. package/lib/base/configs/endpoints.config/Ticket/ticket.endpoint.config.ts +1 -0
  108. package/lib/base/configs/navigation.config/submenu/ticket/config/relatorioCarregamentoBiodiesel.submenu.ts +16 -0
  109. package/lib/base/configs/navigation.config/submenu/ticket/relatorios.submenu.ts +2 -0
  110. package/lib/base/locales/lang/ptBR/modules/Ticket/ticket.nav.ts +1 -0
  111. package/lib/base/services/modules/Ticket/TicketService.ts +8 -0
  112. 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={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
- <span className="flex items-center gap-2">
78
- <span className="size-1.5 rounded-full bg-gray-700 motion-safe:animate-[bounce_1s_ease-in-out_infinite] dark:bg-gray-300"></span>
79
- <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>
80
- <span className="size-1.5 rounded-full bg-gray-700 motion-safe:animate-[bounce_1s_ease-in-out_infinite] dark:bg-gray-300"></span>
81
- </span>
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
- activeRequests++;
19
- if (activeRequests === 1) {
20
- const microfrontendId = getCurrentMicrofrontendId();
21
- const loadingConfig = useLoadingConfigStore.getState();
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
- activeRequests = Math.max(0, activeRequests - 1);
30
- if (activeRequests === 0) {
31
- const microfrontendId = getCurrentMicrofrontendId();
32
- const loadingConfig = useLoadingConfigStore.getState();
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
- activeRequests = Math.max(0, activeRequests - 1);
44
- if (activeRequests === 0) {
45
- const microfrontendId = getCurrentMicrofrontendId();
46
- const loadingConfig = useLoadingConfigStore.getState();
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
- activeRequests = Math.max(0, activeRequests - 1);
55
- if (activeRequests === 0) {
56
- const microfrontendId = getCurrentMicrofrontendId();
57
- const loadingConfig = useLoadingConfigStore.getState();
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: 'Help me sound like an expert for an upcoming trip',
26
- prompt: `I'm going whale watching this weekend and want to appear knowledgeable about killer whales. Tell me a few unique and interesting facts I can share.`,
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: 'Outline an logical sales pitch for a new product',
31
- prompt: `Structure a sales pitch for a hair dryer that's also a microphone. Be concise and organize the sales pitch logically. `,
32
- type: 'writing',
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: 'Help me get organized with a list of 10 tips',
36
- prompt: `Give me 10 tips for room organization. `,
37
- type: 'idea',
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: 'Write code for a specific task, including edge cases',
41
- prompt: `Write a Java function that takes a path as an input and creates a file storing the current system date. Consider edge cases.`,
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-20">
50
+ <div className="max-w-[900px] w-full mx-auto mt-6">
51
51
  <div>
52
- <div className="heading-text text-4xl leading-snug">
53
- <span className="font-semibold bg-gradient-to-r from-indigo-500 to-red-400 bg-clip-text text-transparent text-5xl">
54
- Hello, there
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>How can I help you today?</span>
57
+ <span>Como posso te ajudar hoje?</span>
58
58
  </div>
59
- <div className="mt-8 grid grid-cols-2 xl:grid-cols-4 gap-4">
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-4 justify-between rounded-xl bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 p-5 min-h-40 2xl:min-h-60 cursor-pointer"
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
  >