@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
|
@@ -4,6 +4,7 @@ import { Form, FormItem } from '@/components/ui/Form'
|
|
|
4
4
|
import Input from '@/components/ui/Input'
|
|
5
5
|
import Button from '@/components/ui/Button'
|
|
6
6
|
import { usGenerativeChatStore } from '../store/generativeChatStore'
|
|
7
|
+
import useChatConversations from '../hooks/useChatConversations'
|
|
7
8
|
import { useForm, Controller } from 'react-hook-form'
|
|
8
9
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
9
10
|
import { z } from 'zod'
|
|
@@ -14,12 +15,19 @@ type FormSchema = {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
const validationSchema: ZodType<FormSchema> = z.object({
|
|
17
|
-
title: z.string().min(1, '
|
|
18
|
+
title: z.string().trim().min(1, 'Informe um título para a conversa.'),
|
|
18
19
|
})
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
type ChatHistoryRenameDialogProps = {
|
|
22
|
+
/** Classes do overlay (ex.: subir o z-index acima do widget flutuante). */
|
|
23
|
+
overlayClassName?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ChatHistoryRenameDialog = ({
|
|
27
|
+
overlayClassName,
|
|
28
|
+
}: ChatHistoryRenameDialogProps) => {
|
|
29
|
+
const { renameDialog, setRenameDialog } = usGenerativeChatStore()
|
|
30
|
+
const { renomear } = useChatConversations()
|
|
23
31
|
|
|
24
32
|
const {
|
|
25
33
|
control,
|
|
@@ -51,16 +59,17 @@ const ChatHistoryRenameDialog = () => {
|
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
const onFormSubmit = async ({ title }: FormSchema) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
})
|
|
62
|
+
// Fecha antes da resposta: o rename é otimista e desfaz sozinho se o
|
|
63
|
+
// backend recusar.
|
|
64
|
+
const { id } = renameDialog
|
|
58
65
|
handleDialogClose()
|
|
66
|
+
await renomear(id, title.trim())
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
return (
|
|
62
70
|
<Dialog
|
|
63
71
|
isOpen={renameDialog.open}
|
|
72
|
+
overlayClassName={overlayClassName}
|
|
64
73
|
onClose={handleDialogClose}
|
|
65
74
|
onRequestClose={handleDialogClose}
|
|
66
75
|
>
|
|
@@ -12,6 +12,7 @@ import ApprovalCard from './ApprovalCard'
|
|
|
12
12
|
import DataRequestCard from './DataRequestCard'
|
|
13
13
|
import { usGenerativeChatStore } from '../store/generativeChatStore'
|
|
14
14
|
import useChatSend from '../hooks/useChatSend'
|
|
15
|
+
import useConversationMessages from '../hooks/useConversationMessages'
|
|
15
16
|
import type { Conversation } from '../types'
|
|
16
17
|
import type { ScrollBarRef } from '@/components/ui/ScrollBar'
|
|
17
18
|
|
|
@@ -31,11 +32,21 @@ import type { ScrollBarRef } from '@/components/ui/ScrollBar'
|
|
|
31
32
|
const BALAO_AGENTE =
|
|
32
33
|
'prose rounded-xl bg-gray-100 px-5 py-2.5 text-sm text-gray-900 dark:bg-gray-700 dark:text-gray-100'
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
type ChatViewProps = {
|
|
36
|
+
/**
|
|
37
|
+
* Mostra o drawer de histórico do ChatMobileNav abaixo de xl. O widget
|
|
38
|
+
* flutuante desliga, porque tem o próprio drawer dentro do painel.
|
|
39
|
+
*/
|
|
40
|
+
showMobileNav?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ChatView = ({ showMobileNav = true }: ChatViewProps) => {
|
|
35
44
|
const scrollRef = useRef<ScrollBarRef>(null)
|
|
36
45
|
const { selectedConversation, chatHistory, isTyping, disabledChatFresh } =
|
|
37
46
|
usGenerativeChatStore()
|
|
38
47
|
const { handleSend, handleApproval, handleDataSubmit } = useChatSend()
|
|
48
|
+
// Conversa aberta da lista: as mensagens vêm do backend na primeira vez.
|
|
49
|
+
const { carregando } = useConversationMessages()
|
|
39
50
|
|
|
40
51
|
const scrollToBottom = () => {
|
|
41
52
|
if (scrollRef.current?.getScrollElement) {
|
|
@@ -73,7 +84,7 @@ const ChatView = () => {
|
|
|
73
84
|
|
|
74
85
|
return (
|
|
75
86
|
<Card className="flex-1 h-full" bodyClass="h-full">
|
|
76
|
-
<ChatMobileNav />
|
|
87
|
+
{showMobileNav && <ChatMobileNav />}
|
|
77
88
|
<ChatBox
|
|
78
89
|
ref={scrollRef}
|
|
79
90
|
messageList={messageList}
|
|
@@ -89,10 +100,12 @@ const ChatView = () => {
|
|
|
89
100
|
<AiThinkingIndicator />
|
|
90
101
|
</div>
|
|
91
102
|
)}
|
|
92
|
-
containerClass=
|
|
103
|
+
containerClass={
|
|
104
|
+
showMobileNav ? 'h-[calc(100%-30px)] xl:h-full' : 'h-full'
|
|
105
|
+
}
|
|
93
106
|
messageListClass="h-[calc(100%-100px)] xl:h-[calc(100%-70px)]"
|
|
94
107
|
typing={
|
|
95
|
-
isTyping
|
|
108
|
+
isTyping || carregando
|
|
96
109
|
? {
|
|
97
110
|
id: 'ai',
|
|
98
111
|
name: 'Chat AI',
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import useSWR from 'swr'
|
|
2
|
+
import Notification from '@/components/ui/Notification'
|
|
3
|
+
import toast from '@/components/ui/toast'
|
|
4
|
+
import {
|
|
5
|
+
apiDeleteConversation,
|
|
6
|
+
apiGetConversations,
|
|
7
|
+
apiRenameConversation,
|
|
8
|
+
} from '@/services/AiService'
|
|
9
|
+
import { useSessionUser } from '@/store/authStore'
|
|
10
|
+
import { usGenerativeChatStore } from '../store/generativeChatStore'
|
|
11
|
+
import { resumoParaHistorico } from '../utils'
|
|
12
|
+
import type { ChatHistories, GetConversationsResponse } from '../types'
|
|
13
|
+
|
|
14
|
+
const avisarFalha = (mensagem: string) => {
|
|
15
|
+
toast.push(
|
|
16
|
+
<Notification type="danger" duration={3000}>
|
|
17
|
+
{mensagem}
|
|
18
|
+
</Notification>,
|
|
19
|
+
{ placement: 'top-center' },
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A lista de conversas do usuário, persistida no backend, e as ações sobre ela.
|
|
25
|
+
*
|
|
26
|
+
* Renomear e excluir são otimistas: a tela muda na hora e volta ao estado
|
|
27
|
+
* anterior se o backend recusar.
|
|
28
|
+
*/
|
|
29
|
+
const useChatConversations = () => {
|
|
30
|
+
const {
|
|
31
|
+
setChatHistory,
|
|
32
|
+
setChatHistoryName,
|
|
33
|
+
setSelectedConversation,
|
|
34
|
+
} = usGenerativeChatStore()
|
|
35
|
+
|
|
36
|
+
const userId = useSessionUser((state) => state.user.userId)
|
|
37
|
+
const signedIn = useSessionUser((state) => state.session.signedIn)
|
|
38
|
+
|
|
39
|
+
// O usuário faz parte da chave: cada um tem o próprio cache, e o de quem
|
|
40
|
+
// saiu nunca é servido a quem entrou. Sem sessão, não busca nada.
|
|
41
|
+
//
|
|
42
|
+
// Sem `revalidateIfStale: false` de propósito: cada vez que a lista monta,
|
|
43
|
+
// ela confere o backend. Com ele, o SWR servia o cache antigo, não chamava
|
|
44
|
+
// o `onSuccess`, e a lista só aparecia depois de um F5.
|
|
45
|
+
useSWR(
|
|
46
|
+
signedIn && userId ? ['/ai/chat/conversations', userId] : null,
|
|
47
|
+
// O dono viaja junto com a resposta para o `onSuccess` conferir.
|
|
48
|
+
async ([, dono]: [string, string]) => ({
|
|
49
|
+
dono,
|
|
50
|
+
resumos: await apiGetConversations<GetConversationsResponse>(),
|
|
51
|
+
}),
|
|
52
|
+
{
|
|
53
|
+
revalidateOnFocus: false,
|
|
54
|
+
revalidateOnReconnect: false,
|
|
55
|
+
onSuccess: ({ dono, resumos }) => {
|
|
56
|
+
// Resposta atrasada de quem já saiu: descarta, senão a lista
|
|
57
|
+
// de um usuário cairia na tela do outro.
|
|
58
|
+
if (dono !== useSessionUser.getState().user.userId) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
// Lido na hora, e não da closure: a resposta pode chegar
|
|
62
|
+
// depois de o usuário já ter começado uma conversa.
|
|
63
|
+
const atual = usGenerativeChatStore.getState().chatHistory
|
|
64
|
+
setChatHistory(mesclar(atual, resumos))
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const remover = async (id: string) => {
|
|
70
|
+
const { chatHistory, selectedConversation } =
|
|
71
|
+
usGenerativeChatStore.getState()
|
|
72
|
+
|
|
73
|
+
setChatHistory(chatHistory.filter((item) => item.id !== id))
|
|
74
|
+
if (selectedConversation === id) {
|
|
75
|
+
setSelectedConversation('')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await apiDeleteConversation(id)
|
|
80
|
+
} catch {
|
|
81
|
+
setChatHistory(chatHistory)
|
|
82
|
+
avisarFalha('Não foi possível excluir a conversa.')
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const renomear = async (id: string, title: string) => {
|
|
87
|
+
const anterior = usGenerativeChatStore
|
|
88
|
+
.getState()
|
|
89
|
+
.chatHistory.find((item) => item.id === id)?.title
|
|
90
|
+
|
|
91
|
+
setChatHistoryName({ id, title })
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await apiRenameConversation(id, title)
|
|
95
|
+
} catch {
|
|
96
|
+
if (anterior !== undefined) {
|
|
97
|
+
setChatHistoryName({ id, title: anterior })
|
|
98
|
+
}
|
|
99
|
+
avisarFalha('Não foi possível renomear a conversa.')
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { remover, renomear }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Junta a lista do backend com o que já está na tela.
|
|
108
|
+
*
|
|
109
|
+
* - Conversa que já existe no store fica como está: pode ter mensagens
|
|
110
|
+
* carregadas ou um stream em andamento.
|
|
111
|
+
* - Conversa criada nesta sessão e que o backend ainda não conhece não pode
|
|
112
|
+
* sumir: ela aparece primeiro, como ao ser criada.
|
|
113
|
+
*/
|
|
114
|
+
function mesclar(
|
|
115
|
+
atual: ChatHistories,
|
|
116
|
+
resumos: GetConversationsResponse,
|
|
117
|
+
): ChatHistories {
|
|
118
|
+
const noStore = new Map(atual.map((chat) => [chat.id, chat]))
|
|
119
|
+
const doBackend = new Set(resumos.map((resumo) => resumo.id))
|
|
120
|
+
|
|
121
|
+
const soNaTela = atual.filter((chat) => !doBackend.has(chat.id))
|
|
122
|
+
const daLista = resumos.map(
|
|
123
|
+
(resumo) => noStore.get(resumo.id) ?? resumoParaHistorico(resumo),
|
|
124
|
+
)
|
|
125
|
+
return [...soNaTela, ...daLista]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default useChatConversations
|
|
@@ -3,6 +3,7 @@ import { resumeChat, streamChat } from '@/services/AiService'
|
|
|
3
3
|
import { useAuth } from '@/auth'
|
|
4
4
|
import dayjs from 'dayjs'
|
|
5
5
|
import uniqueId from 'lodash/uniqueId'
|
|
6
|
+
import { novoIdDeConversa } from '../utils'
|
|
6
7
|
import type { ChatEvent, Conversation, ToolCall } from '../types'
|
|
7
8
|
|
|
8
9
|
/** Resposta genérica exibida no chat quando a comunicação com a IA falha. */
|
|
@@ -203,14 +204,14 @@ const useChatSend = () => {
|
|
|
203
204
|
creteMyMessage(selectedConversation, prompt)
|
|
204
205
|
await sendMessage(selectedConversation, prompt)
|
|
205
206
|
} else {
|
|
206
|
-
const newId =
|
|
207
|
+
const newId = novoIdDeConversa()
|
|
207
208
|
pushChatHistory({
|
|
208
209
|
id: newId,
|
|
209
210
|
title: prompt,
|
|
210
211
|
lastConversation: '',
|
|
211
212
|
createdTime: dayjs().unix(),
|
|
212
213
|
updatedTime: dayjs().unix(),
|
|
213
|
-
enable:
|
|
214
|
+
enable: true,
|
|
214
215
|
})
|
|
215
216
|
creteMyMessage(newId, prompt)
|
|
216
217
|
await createConversation(newId, prompt)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { apiGetChatHistory } from '@/services/AiService'
|
|
3
|
+
import { usGenerativeChatStore } from '../store/generativeChatStore'
|
|
4
|
+
import { mensagemDoHistorico } from '../utils'
|
|
5
|
+
import type { Conversation, HistoryChat } from '../types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Busca as mensagens da conversa selecionada quando ela veio da lista do
|
|
9
|
+
* backend e ainda não foi aberta nesta sessão.
|
|
10
|
+
*
|
|
11
|
+
* Busca uma vez por conversa: depois disso `loaded` fica true e as mensagens
|
|
12
|
+
* novas chegam pelo streaming, como em qualquer conversa.
|
|
13
|
+
*/
|
|
14
|
+
const useConversationMessages = () => {
|
|
15
|
+
const { selectedConversation, chatHistory, setConversationMessages } =
|
|
16
|
+
usGenerativeChatStore()
|
|
17
|
+
const [carregando, setCarregando] = useState(false)
|
|
18
|
+
|
|
19
|
+
const pendente =
|
|
20
|
+
chatHistory.find((chat) => chat.id === selectedConversation)?.loaded ===
|
|
21
|
+
false
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!selectedConversation || !pendente) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Trocar de conversa no meio da busca não pode gravar a resposta
|
|
29
|
+
// atrasada na tela errada.
|
|
30
|
+
let cancelado = false
|
|
31
|
+
setCarregando(true)
|
|
32
|
+
|
|
33
|
+
// O usuário pode ter enviado algo enquanto a busca corria: o que já
|
|
34
|
+
// está na tela vem depois do histórico, e não some.
|
|
35
|
+
const gravar = (antigas: Conversation[]) => {
|
|
36
|
+
const naTela =
|
|
37
|
+
usGenerativeChatStore
|
|
38
|
+
.getState()
|
|
39
|
+
.chatHistory.find((chat) => chat.id === selectedConversation)
|
|
40
|
+
?.conversation ?? []
|
|
41
|
+
setConversationMessages(selectedConversation, [...antigas, ...naTela])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
apiGetChatHistory<HistoryChat[]>(selectedConversation)
|
|
45
|
+
.then((resposta) => {
|
|
46
|
+
if (cancelado) return
|
|
47
|
+
gravar((resposta[0]?.conversation ?? []).map(mensagemDoHistorico))
|
|
48
|
+
})
|
|
49
|
+
.catch(() => {
|
|
50
|
+
// Marca como carregada mesmo assim: sem isto, cada render
|
|
51
|
+
// tentaria de novo. A conversa segue utilizável, porque a
|
|
52
|
+
// memória está no backend.
|
|
53
|
+
if (!cancelado) gravar([])
|
|
54
|
+
})
|
|
55
|
+
.finally(() => {
|
|
56
|
+
if (!cancelado) setCarregando(false)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
cancelado = true
|
|
61
|
+
setCarregando(false)
|
|
62
|
+
}
|
|
63
|
+
}, [selectedConversation, pendente, setConversationMessages])
|
|
64
|
+
|
|
65
|
+
return { carregando }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default useConversationMessages
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { create } from 'zustand'
|
|
2
|
+
import { useSessionUser } from '@/store/authStore'
|
|
2
3
|
import type { ChatHistories, ChatHistory, Conversation } from '../types'
|
|
3
4
|
|
|
4
5
|
type RenameDialog = {
|
|
@@ -24,12 +25,16 @@ type GenerativeChatAction = {
|
|
|
24
25
|
setIsTyping: (payload: boolean) => void
|
|
25
26
|
pushChatHistory: (payload: ChatHistory) => void
|
|
26
27
|
pushConversation: (id: string, conversation: Conversation) => void
|
|
28
|
+
/** Grava as mensagens buscadas no backend e marca a conversa como carregada. */
|
|
29
|
+
setConversationMessages: (id: string, conversation: Conversation[]) => void
|
|
27
30
|
updateConversation: (
|
|
28
31
|
chatId: string,
|
|
29
32
|
messageId: string,
|
|
30
33
|
patch: Partial<Conversation>,
|
|
31
34
|
) => void
|
|
32
35
|
disabledChatFresh: (id: string) => void
|
|
36
|
+
/** Volta ao estado inicial. Chamado sozinho quando o usuário da sessão muda. */
|
|
37
|
+
reset: () => void
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
const initialState: GenerativeChatState = {
|
|
@@ -48,6 +53,14 @@ export const usGenerativeChatStore = create<
|
|
|
48
53
|
GenerativeChatState & GenerativeChatAction
|
|
49
54
|
>((set, get) => ({
|
|
50
55
|
...initialState,
|
|
56
|
+
// Arrays novos: `setSelectedConversationRecord` muta o array no lugar, e
|
|
57
|
+
// reusar o do initialState traria de volta o que ele acumulou.
|
|
58
|
+
reset: () =>
|
|
59
|
+
set(() => ({
|
|
60
|
+
...initialState,
|
|
61
|
+
selectedConversationRecord: [],
|
|
62
|
+
chatHistory: [],
|
|
63
|
+
})),
|
|
51
64
|
setSelectedConversation: (payload) =>
|
|
52
65
|
set(() => ({ selectedConversation: payload })),
|
|
53
66
|
setChatHistory: (payload) => set(() => ({ chatHistory: payload })),
|
|
@@ -74,8 +87,18 @@ export const usGenerativeChatStore = create<
|
|
|
74
87
|
|
|
75
88
|
return { selectedConversationRecord: record }
|
|
76
89
|
}),
|
|
90
|
+
// Conversa criada nesta sessão já nasce carregada: as mensagens estão aqui,
|
|
91
|
+
// não há o que buscar no backend.
|
|
77
92
|
pushChatHistory: (payload) =>
|
|
78
|
-
set(() => ({
|
|
93
|
+
set(() => ({
|
|
94
|
+
chatHistory: [{ loaded: true, ...payload }, ...get().chatHistory],
|
|
95
|
+
})),
|
|
96
|
+
setConversationMessages: (id, conversation) =>
|
|
97
|
+
set(() => ({
|
|
98
|
+
chatHistory: get().chatHistory.map((chat) =>
|
|
99
|
+
chat.id === id ? { ...chat, conversation, loaded: true } : chat,
|
|
100
|
+
),
|
|
101
|
+
})),
|
|
79
102
|
pushConversation: (id, conversation) =>
|
|
80
103
|
set(() => ({
|
|
81
104
|
chatHistory: get().chatHistory.map((chat) =>
|
|
@@ -131,3 +154,23 @@ export const usGenerativeChatStore = create<
|
|
|
131
154
|
return { chatHistory }
|
|
132
155
|
}),
|
|
133
156
|
}))
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* O chat pertence ao usuário da sessão: trocou o usuário, zera o chat.
|
|
160
|
+
*
|
|
161
|
+
* Login e logout acontecem sem recarregar a página, e este store é um singleton
|
|
162
|
+
* do módulo. Sem isto, o próximo usuário via as conversas do anterior.
|
|
163
|
+
*
|
|
164
|
+
* Olha o `signedIn` além do `userId` porque o logout faz `setUser({})`, e o
|
|
165
|
+
* `setUser` faz MERGE: o `userId` antigo continua lá depois de sair.
|
|
166
|
+
*
|
|
167
|
+
* É uma assinatura, e não um efeito de componente, para zerar ANTES do próximo
|
|
168
|
+
* render: a lista antiga nem chega a piscar na tela.
|
|
169
|
+
*/
|
|
170
|
+
useSessionUser.subscribe((atual, anterior) => {
|
|
171
|
+
const trocouUsuario = atual.user.userId !== anterior.user.userId
|
|
172
|
+
const saiu = anterior.session.signedIn && !atual.session.signedIn
|
|
173
|
+
if (trocouUsuario || saiu) {
|
|
174
|
+
usGenerativeChatStore.getState().reset()
|
|
175
|
+
}
|
|
176
|
+
})
|
|
@@ -131,8 +131,36 @@ export type ChatHistory = {
|
|
|
131
131
|
createdTime: number
|
|
132
132
|
updatedTime: number
|
|
133
133
|
enable: boolean
|
|
134
|
+
/**
|
|
135
|
+
* false quando a conversa veio da lista do backend e as mensagens ainda
|
|
136
|
+
* não foram buscadas. Conversas criadas nesta sessão já nascem carregadas.
|
|
137
|
+
*/
|
|
138
|
+
loaded?: boolean
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
export type ChatHistories = ChatHistory[]
|
|
137
142
|
|
|
138
143
|
export type GetChatHistoryResponse = ChatHistories
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Mensagem como o backend devolve no /ai/chat/history: o `timestamp` vem em
|
|
147
|
+
* segundos (epoch), e não como Date.
|
|
148
|
+
*/
|
|
149
|
+
export type HistoryMessage = Omit<Conversation, 'timestamp'> & {
|
|
150
|
+
timestamp: number
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Uma conversa do /ai/chat/history, com as mensagens no formato do backend. */
|
|
154
|
+
export type HistoryChat = Omit<ChatHistory, 'conversation'> & {
|
|
155
|
+
conversation: HistoryMessage[]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Resumo de uma conversa, como vem do GET /ai/chat/conversations. */
|
|
159
|
+
export type ConversationSummary = {
|
|
160
|
+
id: string
|
|
161
|
+
title: string
|
|
162
|
+
createdTime: number
|
|
163
|
+
updatedTime: number
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export type GetConversationsResponse = ConversationSummary[]
|
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
import dayjs from 'dayjs'
|
|
2
|
+
import type {
|
|
3
|
+
ChatHistory,
|
|
4
|
+
Conversation,
|
|
5
|
+
ConversationSummary,
|
|
6
|
+
HistoryMessage,
|
|
7
|
+
} from './types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Id de uma conversa nova.
|
|
11
|
+
*
|
|
12
|
+
* Precisa ser único entre recargas: o backend guarda a memória por este id.
|
|
13
|
+
* Um contador por página (como o `uniqueId` do lodash) recomeça no F5, e o
|
|
14
|
+
* "novo chat" herdaria a memória de uma conversa antiga.
|
|
15
|
+
*/
|
|
16
|
+
export const novoIdDeConversa = (): string => crypto.randomUUID()
|
|
17
|
+
|
|
18
|
+
/** Conversa da lista do backend, ainda sem as mensagens. */
|
|
19
|
+
export const resumoParaHistorico = (
|
|
20
|
+
resumo: ConversationSummary,
|
|
21
|
+
): ChatHistory => ({
|
|
22
|
+
...resumo,
|
|
23
|
+
lastConversation: '',
|
|
24
|
+
enable: true,
|
|
25
|
+
loaded: false,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
/** Mensagem do /ai/chat/history no formato do chat (timestamp em Date). */
|
|
29
|
+
export const mensagemDoHistorico = (mensagem: HistoryMessage): Conversation => ({
|
|
30
|
+
...mensagem,
|
|
31
|
+
timestamp: dayjs.unix(mensagem.timestamp).toDate(),
|
|
32
|
+
})
|
|
33
|
+
|
|
1
34
|
export const getStringPosition = (
|
|
2
35
|
string: string,
|
|
3
36
|
subString: string,
|
|
@@ -17,6 +17,8 @@ const contratoFrete = {
|
|
|
17
17
|
enviarSap: {endpoint: `${CONTRATO_FRETE_URL}/enviar-sap/`, method: 'get'},
|
|
18
18
|
uploadArquivo: {endpoint: `${CONTRATO_FRETE_URL}/upload-arquivo`, method: 'post'},
|
|
19
19
|
findDocumento: {endpoint: `${CONTRATO_FRETE_URL}/find/documento/`, method: 'get'},
|
|
20
|
+
updateStatusPedagio: {endpoint: `${CONTRATO_FRETE_URL}/update/status-pedagio`, method: 'put'},
|
|
21
|
+
pagamentoPedagio: {endpoint: `${CONTRATO_FRETE_URL}/pagamento-pedagio`, method: 'post'},
|
|
20
22
|
}
|
|
21
23
|
export default contratoFrete
|
|
22
24
|
|
|
@@ -126,4 +126,18 @@ export async function apiFindDocumentoViagem(documento: string, tipo: string) {
|
|
|
126
126
|
url: `${CONTRATO_FRETE_PROJECT_ENDPOINT.findDocumento.endpoint}${documento}/${tipo}`,
|
|
127
127
|
method: CONTRATO_FRETE_PROJECT_ENDPOINT.findDocumento.method,
|
|
128
128
|
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function apiUpdateStatusPedagio(id: number, statusPedagio: string) {
|
|
132
|
+
return ApiService.fetchDataWithAxios<TContratoFrete>({
|
|
133
|
+
url: `${CONTRATO_FRETE_PROJECT_ENDPOINT.updateStatusPedagio.endpoint}/${id}/${statusPedagio}`,
|
|
134
|
+
method: CONTRATO_FRETE_PROJECT_ENDPOINT.updateStatusPedagio.method,
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function apiPagamentoPedagio(id: number) {
|
|
139
|
+
return ApiService.fetchDataWithAxios<TContratoFrete>({
|
|
140
|
+
url: `${CONTRATO_FRETE_PROJECT_ENDPOINT.pagamentoPedagio.endpoint}/${id}`,
|
|
141
|
+
method: CONTRATO_FRETE_PROJECT_ENDPOINT.pagamentoPedagio.method,
|
|
142
|
+
})
|
|
129
143
|
}
|
package/package.json
CHANGED