@orbe-agro/client-core 5.6.239 → 5.6.241
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 +116 -3
- package/dist/@ecme/assets/styles/tailwind/index.css +119 -3
- package/dist/@ecme/components/template/Footer.js +1 -1
- package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.js +0 -2
- package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.js.map +1 -1
- package/dist/@ecme/components/view/ChatBox/ChatBox.js +2 -0
- package/dist/@ecme/components/view/ChatBox/ChatBox.js.map +1 -1
- package/dist/@ecme/components/view/ChatBox/components/MessageList.js +2 -1
- package/dist/@ecme/components/view/ChatBox/components/MessageList.js.map +1 -1
- package/dist/@ecme/index.js +3 -4
- package/dist/@ecme/services/AiService.js +53 -24
- package/dist/@ecme/services/AiService.js.map +1 -1
- package/dist/@ecme/services/aiAuthHeaders.js +35 -0
- package/dist/@ecme/services/aiAuthHeaders.js.map +1 -0
- package/dist/@ecme/services/axios/AxiosRequestIntrceptorConfigCallback.js +4 -27
- package/dist/@ecme/services/axios/AxiosRequestIntrceptorConfigCallback.js.map +1 -1
- package/dist/@ecme/services/index.js +4 -5
- package/dist/@ecme/services/sseParser.js +42 -0
- package/dist/@ecme/services/sseParser.js.map +1 -0
- package/dist/@ecme/views/concepts/ai/Chat/components/AiErrorMessage.js +15 -0
- package/dist/@ecme/views/concepts/ai/Chat/components/AiErrorMessage.js.map +1 -0
- package/dist/@ecme/views/concepts/ai/Chat/components/ApprovalCard.js +73 -0
- package/dist/@ecme/views/concepts/ai/Chat/components/ApprovalCard.js.map +1 -0
- package/dist/@ecme/views/concepts/ai/Chat/components/ChatView.js +37 -10
- package/dist/@ecme/views/concepts/ai/Chat/components/ChatView.js.map +1 -1
- package/dist/@ecme/views/concepts/ai/Chat/components/ToolCallChip.js +153 -0
- package/dist/@ecme/views/concepts/ai/Chat/components/ToolCallChip.js.map +1 -0
- package/dist/@ecme/views/concepts/ai/Chat/components/toolPresentation.js +99 -0
- package/dist/@ecme/views/concepts/ai/Chat/components/toolPresentation.js.map +1 -0
- package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatSend.js +111 -17
- package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatSend.js.map +1 -1
- package/dist/@ecme/views/concepts/ai/Chat/store/generativeChatStore.js +29 -13
- package/dist/@ecme/views/concepts/ai/Chat/store/generativeChatStore.js.map +1 -1
- package/dist/@types/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.d.ts.map +1 -1
- package/dist/@types/@ecme/components/view/ChatBox/ChatBox.d.ts.map +1 -1
- package/dist/@types/@ecme/components/view/ChatBox/components/MessageList.d.ts +2 -0
- package/dist/@types/@ecme/components/view/ChatBox/components/MessageList.d.ts.map +1 -1
- package/dist/@types/@ecme/services/AiService.d.ts +28 -15
- package/dist/@types/@ecme/services/AiService.d.ts.map +1 -1
- package/dist/@types/@ecme/services/aiAuthHeaders.d.ts +12 -0
- package/dist/@types/@ecme/services/aiAuthHeaders.d.ts.map +1 -0
- package/dist/@types/@ecme/services/axios/AxiosRequestIntrceptorConfigCallback.d.ts +6 -0
- package/dist/@types/@ecme/services/axios/AxiosRequestIntrceptorConfigCallback.d.ts.map +1 -1
- package/dist/@types/@ecme/services/sseParser.d.ts +20 -0
- package/dist/@types/@ecme/services/sseParser.d.ts.map +1 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/AiErrorMessage.d.ts +8 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/AiErrorMessage.d.ts.map +1 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/ApprovalCard.d.ts +15 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/ApprovalCard.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/ToolCallChip.d.ts +7 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/ToolCallChip.d.ts.map +1 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/toolPresentation.d.ts +14 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/toolPresentation.d.ts.map +1 -0
- package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useChatSend.d.ts +2 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useChatSend.d.ts.map +1 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/store/generativeChatStore.d.ts +1 -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 +96 -23
- package/dist/@types/@ecme/views/concepts/ai/Chat/types.d.ts.map +1 -1
- package/dist/@types/base/@types/models/transportadora/irpf.d.ts +1 -0
- package/dist/@types/base/@types/models/transportadora/irpf.d.ts.map +1 -1
- package/dist/@types/base/@types/models/transportadora/irpfFaixaDesconto.d.ts +0 -1
- package/dist/@types/base/@types/models/transportadora/irpfFaixaDesconto.d.ts.map +1 -1
- package/dist/base/assets/styles/init.css +128 -3
- package/lib/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.tsx +0 -2
- package/lib/@ecme/components/view/ChatBox/ChatBox.tsx +2 -0
- package/lib/@ecme/components/view/ChatBox/components/MessageList.tsx +8 -4
- package/lib/@ecme/services/AiService.ts +89 -44
- package/lib/@ecme/services/aiAuthHeaders.ts +58 -0
- package/lib/@ecme/services/axios/AxiosRequestIntrceptorConfigCallback.ts +10 -39
- package/lib/@ecme/services/sseParser.ts +75 -0
- package/lib/@ecme/views/concepts/ai/Chat/components/AiErrorMessage.tsx +28 -0
- package/lib/@ecme/views/concepts/ai/Chat/components/ApprovalCard.tsx +103 -0
- package/lib/@ecme/views/concepts/ai/Chat/components/ChatView.tsx +74 -10
- package/lib/@ecme/views/concepts/ai/Chat/components/ToolCallChip.tsx +218 -0
- package/lib/@ecme/views/concepts/ai/Chat/components/toolPresentation.ts +137 -0
- package/lib/@ecme/views/concepts/ai/Chat/hooks/useChatSend.ts +175 -23
- package/lib/@ecme/views/concepts/ai/Chat/store/generativeChatStore.ts +41 -13
- package/lib/@ecme/views/concepts/ai/Chat/types.ts +82 -29
- package/lib/base/@types/models/transportadora/irpf.ts +1 -0
- package/lib/base/@types/models/transportadora/irpfFaixaDesconto.ts +0 -1
- package/package.json +1 -1
- package/dist/@ecme/views/concepts/ai/Chat/components/ModelSelect.js +0 -48
- package/dist/@ecme/views/concepts/ai/Chat/components/ModelSelect.js.map +0 -1
- package/dist/@ecme/views/concepts/ai/Chat/store/aiModelStore.js +0 -15
- package/dist/@ecme/views/concepts/ai/Chat/store/aiModelStore.js.map +0 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/ModelSelect.d.ts +0 -8
- package/dist/@types/@ecme/views/concepts/ai/Chat/components/ModelSelect.d.ts.map +0 -1
- package/dist/@types/@ecme/views/concepts/ai/Chat/store/aiModelStore.d.ts +0 -25
- package/dist/@types/@ecme/views/concepts/ai/Chat/store/aiModelStore.d.ts.map +0 -1
- package/lib/@ecme/views/concepts/ai/Chat/components/ModelSelect.tsx +0 -66
- package/lib/@ecme/views/concepts/ai/Chat/store/aiModelStore.ts +0 -23
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser de Server-Sent Events sobre um ReadableStream.
|
|
3
|
+
*
|
|
4
|
+
* O EventSource nativo do browser nao serve aqui: ele so faz GET e nao aceita
|
|
5
|
+
* headers, e o chat e um POST autenticado. Entao lemos o corpo do `fetch` na
|
|
6
|
+
* mao.
|
|
7
|
+
*
|
|
8
|
+
* Duas bordas do protocolo que uma implementacao ingenua erra:
|
|
9
|
+
*
|
|
10
|
+
* 1. um chunk da rede pode cortar um evento no meio — por isso o resto fica no
|
|
11
|
+
* buffer ate chegar o proximo `\n\n`;
|
|
12
|
+
* 2. um chunk pode trazer varios eventos de uma vez.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type SseEvent = {
|
|
16
|
+
event: string
|
|
17
|
+
data: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SEPARADOR = /\r?\n\r?\n/
|
|
21
|
+
|
|
22
|
+
/** Le o stream ate o fim, chamando `onEvent` a cada evento completo. */
|
|
23
|
+
export async function readSse(
|
|
24
|
+
body: ReadableStream<Uint8Array>,
|
|
25
|
+
onEvent: (evento: SseEvent) => void,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
const reader = body.getReader()
|
|
28
|
+
const decoder = new TextDecoder()
|
|
29
|
+
let buffer = ''
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
for (;;) {
|
|
33
|
+
const { done, value } = await reader.read()
|
|
34
|
+
|
|
35
|
+
if (done) {
|
|
36
|
+
// O ultimo evento pode nao vir seguido de linha em branco.
|
|
37
|
+
emitir(buffer, onEvent)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
buffer += decoder.decode(value, { stream: true })
|
|
42
|
+
|
|
43
|
+
const blocos = buffer.split(SEPARADOR)
|
|
44
|
+
// O ultimo pedaco pode estar incompleto: volta para o buffer.
|
|
45
|
+
buffer = blocos.pop() ?? ''
|
|
46
|
+
blocos.forEach((bloco) => emitir(bloco, onEvent))
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
reader.releaseLock()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Converte um bloco "event: x\ndata: y" em SseEvent. */
|
|
54
|
+
function emitir(bloco: string, onEvent: (evento: SseEvent) => void) {
|
|
55
|
+
if (!bloco.trim()) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let event = 'message'
|
|
60
|
+
const dados: string[] = []
|
|
61
|
+
|
|
62
|
+
bloco.split(/\r?\n/).forEach((linha) => {
|
|
63
|
+
if (linha.startsWith('event:')) {
|
|
64
|
+
event = linha.slice(6).trim()
|
|
65
|
+
} else if (linha.startsWith('data:')) {
|
|
66
|
+
// O espaco depois de "data:" e opcional no protocolo.
|
|
67
|
+
dados.push(linha.slice(5).replace(/^ /, ''))
|
|
68
|
+
}
|
|
69
|
+
// Linhas iniciadas por ":" sao comentarios (keep-alive) e sao ignoradas.
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
if (dados.length) {
|
|
73
|
+
onEvent({ event, data: dados.join('\n') })
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { TbPlugConnectedX } from 'react-icons/tb'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mensagem de erro do agente Orbe AI — exibida no lugar da resposta quando a
|
|
5
|
+
* comunicação com o serviço de IA falha, com acento vermelho para sinalizar
|
|
6
|
+
* o erro sem quebrar o tom visual do chat.
|
|
7
|
+
*/
|
|
8
|
+
const AiErrorMessage = () => {
|
|
9
|
+
return (
|
|
10
|
+
<span className="flex items-start gap-3">
|
|
11
|
+
<span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full bg-red-100 text-red-500 dark:bg-red-500/15 dark:text-red-400">
|
|
12
|
+
<TbPlugConnectedX size={18} />
|
|
13
|
+
</span>
|
|
14
|
+
<span className="flex flex-col gap-0.5">
|
|
15
|
+
<span className="text-sm font-semibold text-red-600 dark:text-red-400">
|
|
16
|
+
Falha na comunicação
|
|
17
|
+
</span>
|
|
18
|
+
<span className="text-sm text-red-500/90 dark:text-red-300/90">
|
|
19
|
+
Desculpe, estou com um pequeno problema de comunicação e
|
|
20
|
+
não consigo te atender no momento. Tente novamente em
|
|
21
|
+
instantes.
|
|
22
|
+
</span>
|
|
23
|
+
</span>
|
|
24
|
+
</span>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default AiErrorMessage
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { TbLock } from 'react-icons/tb'
|
|
3
|
+
import Button from '@/components/ui/Button'
|
|
4
|
+
import { camposVisiveis, iconeFerramenta, rotularFerramenta } from './toolPresentation'
|
|
5
|
+
import type { PendingApproval } from '../types'
|
|
6
|
+
|
|
7
|
+
type ApprovalCardProps = {
|
|
8
|
+
approval: PendingApproval
|
|
9
|
+
onDecide: (approved: boolean) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Confirmacao de uma operacao que grava no ERP.
|
|
14
|
+
*
|
|
15
|
+
* O agente fica PARADO enquanto este card estiver na tela — so volta a
|
|
16
|
+
* responder depois do clique. Por isso ele nao usa a paleta de alerta: nao e um
|
|
17
|
+
* aviso de erro, e uma decisao de negocio que o usuario precisa tomar.
|
|
18
|
+
*/
|
|
19
|
+
const ApprovalCard = ({ approval, onDecide }: ApprovalCardProps) => {
|
|
20
|
+
const [decisao, setDecisao] = useState<'aprovar' | 'negar' | null>(null)
|
|
21
|
+
|
|
22
|
+
const decidir = (aprovado: boolean) => {
|
|
23
|
+
setDecisao(aprovado ? 'aprovar' : 'negar')
|
|
24
|
+
onDecide(aprovado)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="not-prose mt-1 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
|
29
|
+
<div className="flex items-center gap-2 border-b border-gray-200 px-4 py-2.5 dark:border-gray-700">
|
|
30
|
+
<TbLock size={16} className="text-gray-400 dark:text-gray-500" />
|
|
31
|
+
<span className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
|
32
|
+
Confirmação necessária
|
|
33
|
+
</span>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div className="px-4 py-3.5">
|
|
37
|
+
{approval.calls.map((call) => {
|
|
38
|
+
const Icone = iconeFerramenta(call.name)
|
|
39
|
+
const campos = camposVisiveis(call.args)
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div key={call.id} className="mb-4 last:mb-0">
|
|
43
|
+
<div className="mb-3 flex items-start gap-2.5">
|
|
44
|
+
<span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary-subtle text-primary">
|
|
45
|
+
<Icone size={17} />
|
|
46
|
+
</span>
|
|
47
|
+
<div className="min-w-0">
|
|
48
|
+
<div className="font-semibold text-gray-900 dark:text-gray-100">
|
|
49
|
+
{rotularFerramenta(call.name)}
|
|
50
|
+
</div>
|
|
51
|
+
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
52
|
+
Revise os dados antes de confirmar.
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<dl className="divide-y divide-gray-100 rounded-lg border border-gray-100 dark:divide-gray-700 dark:border-gray-700">
|
|
58
|
+
{campos.map((campo) => (
|
|
59
|
+
<div
|
|
60
|
+
key={campo.chave}
|
|
61
|
+
className="flex items-baseline justify-between gap-4 px-3 py-2"
|
|
62
|
+
>
|
|
63
|
+
<dt className="shrink-0 text-sm text-gray-500 dark:text-gray-400">
|
|
64
|
+
{campo.rotulo}
|
|
65
|
+
</dt>
|
|
66
|
+
<dd className="truncate text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
67
|
+
{campo.valor}
|
|
68
|
+
</dd>
|
|
69
|
+
</div>
|
|
70
|
+
))}
|
|
71
|
+
</dl>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
})}
|
|
75
|
+
|
|
76
|
+
<div className="mt-4 flex items-center gap-2">
|
|
77
|
+
<Button
|
|
78
|
+
variant="solid"
|
|
79
|
+
size="sm"
|
|
80
|
+
loading={decisao === 'aprovar'}
|
|
81
|
+
disabled={decisao !== null}
|
|
82
|
+
onClick={() => decidir(true)}
|
|
83
|
+
>
|
|
84
|
+
Confirmar
|
|
85
|
+
</Button>
|
|
86
|
+
<Button
|
|
87
|
+
size="sm"
|
|
88
|
+
loading={decisao === 'negar'}
|
|
89
|
+
disabled={decisao !== null}
|
|
90
|
+
onClick={() => decidir(false)}
|
|
91
|
+
>
|
|
92
|
+
Cancelar
|
|
93
|
+
</Button>
|
|
94
|
+
<span className="ml-auto text-xs text-gray-400 dark:text-gray-500">
|
|
95
|
+
Nada é gravado até você confirmar
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default ApprovalCard
|
|
@@ -6,15 +6,35 @@ import ChatMobileNav from './ChatMobileNav'
|
|
|
6
6
|
import ChatCustomContent from './ChatCustomContent'
|
|
7
7
|
import ChatCustomAction from './ChatCustomAction'
|
|
8
8
|
import AiThinkingIndicator from './AiThinkingIndicator'
|
|
9
|
+
import AiErrorMessage from './AiErrorMessage'
|
|
10
|
+
import ToolCallChip from './ToolCallChip'
|
|
11
|
+
import ApprovalCard from './ApprovalCard'
|
|
9
12
|
import { usGenerativeChatStore } from '../store/generativeChatStore'
|
|
10
13
|
import useChatSend from '../hooks/useChatSend'
|
|
14
|
+
import type { Conversation } from '../types'
|
|
11
15
|
import type { ScrollBarRef } from '@/components/ui/ScrollBar'
|
|
12
16
|
|
|
17
|
+
/**
|
|
18
|
+
* O balao de fala do agente, desenhado aqui em vez de vir do ChatBox.
|
|
19
|
+
*
|
|
20
|
+
* O `botBubbleClass` abaixo zera o balao que o Message desenharia, para os chips
|
|
21
|
+
* de ferramenta e o card de aprovacao ficarem FORA dele — antes tudo dividia a
|
|
22
|
+
* mesma caixa cinza e as ferramentas eram lidas como uma segunda mensagem.
|
|
23
|
+
* Usado tambem pelo indicador de "pensando", que passa pelo mesmo balao zerado.
|
|
24
|
+
*
|
|
25
|
+
* Repete `prose text-sm` do balao original de proposito: o reset de margem do
|
|
26
|
+
* @tailwindcss/typography e `.prose > :first-child`, filho DIRETO. Sem o `prose`
|
|
27
|
+
* aqui, o markdown ficaria um nivel abaixo do container e o primeiro paragrafo
|
|
28
|
+
* voltaria a ter margem, inflando todos os baloes.
|
|
29
|
+
*/
|
|
30
|
+
const BALAO_AGENTE =
|
|
31
|
+
'prose rounded-xl bg-gray-100 px-5 py-2.5 text-sm text-gray-900 dark:bg-gray-700 dark:text-gray-100'
|
|
32
|
+
|
|
13
33
|
const ChatView = () => {
|
|
14
34
|
const scrollRef = useRef<ScrollBarRef>(null)
|
|
15
35
|
const { selectedConversation, chatHistory, isTyping, disabledChatFresh } =
|
|
16
36
|
usGenerativeChatStore()
|
|
17
|
-
const { handleSend } = useChatSend()
|
|
37
|
+
const { handleSend, handleApproval } = useChatSend()
|
|
18
38
|
|
|
19
39
|
const scrollToBottom = () => {
|
|
20
40
|
if (scrollRef.current?.getScrollElement) {
|
|
@@ -61,7 +81,13 @@ const ChatView = () => {
|
|
|
61
81
|
showAvatar={false}
|
|
62
82
|
avatarGap={false}
|
|
63
83
|
myBubbleClass="bg-primary text-white"
|
|
64
|
-
|
|
84
|
+
botBubbleClass="bg-transparent dark:bg-transparent p-0 rounded-none"
|
|
85
|
+
errorBubbleClass="bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/20"
|
|
86
|
+
renderTyping={() => (
|
|
87
|
+
<div className={BALAO_AGENTE}>
|
|
88
|
+
<AiThinkingIndicator />
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
65
91
|
containerClass="h-[calc(100%-30px)] xl:h-full"
|
|
66
92
|
messageListClass="h-[calc(100%-100px)] xl:h-[calc(100%-70px)]"
|
|
67
93
|
typing={
|
|
@@ -74,22 +100,60 @@ const ChatView = () => {
|
|
|
74
100
|
: false
|
|
75
101
|
}
|
|
76
102
|
customRenderer={(message) => {
|
|
103
|
+
if (message.isError) {
|
|
104
|
+
return <AiErrorMessage />
|
|
105
|
+
}
|
|
106
|
+
|
|
77
107
|
if (message.sender.id === 'ai') {
|
|
108
|
+
// As ferramentas e a aprovacao chegam pelo streaming e
|
|
109
|
+
// vivem no Conversation; o tipo do ChatBox nao os conhece.
|
|
110
|
+
const { tools, pendingApproval } = message as Conversation
|
|
111
|
+
// Enquanto as ferramentas rodam o conteudo ainda e '':
|
|
112
|
+
// sem esta guarda sobraria um balao cinza vazio.
|
|
113
|
+
const temTexto = Boolean(
|
|
114
|
+
(message.content as string)?.trim(),
|
|
115
|
+
)
|
|
116
|
+
|
|
78
117
|
return (
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
118
|
+
<div className="flex flex-col items-start gap-1.5">
|
|
119
|
+
{tools?.map((tool) => (
|
|
120
|
+
<ToolCallChip key={tool.id} tool={tool} />
|
|
121
|
+
))}
|
|
122
|
+
{temTexto && (
|
|
123
|
+
<div className={BALAO_AGENTE}>
|
|
124
|
+
<ChatCustomContent
|
|
125
|
+
content={message.content as string}
|
|
126
|
+
triggerTyping={
|
|
127
|
+
message.fresh
|
|
128
|
+
? message.fresh
|
|
129
|
+
: false
|
|
130
|
+
}
|
|
131
|
+
onFinish={() =>
|
|
132
|
+
handleFinish(message.id)
|
|
133
|
+
}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
{pendingApproval && (
|
|
138
|
+
<ApprovalCard
|
|
139
|
+
approval={pendingApproval}
|
|
140
|
+
onDecide={(aprovado) =>
|
|
141
|
+
handleApproval(
|
|
142
|
+
selectedConversation,
|
|
143
|
+
message.id,
|
|
144
|
+
aprovado,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
/>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
86
150
|
)
|
|
87
151
|
}
|
|
88
152
|
|
|
89
153
|
return message.content
|
|
90
154
|
}}
|
|
91
155
|
customAction={(message) => {
|
|
92
|
-
if (message.sender.id === 'ai') {
|
|
156
|
+
if (message.sender.id === 'ai' && !message.isError) {
|
|
93
157
|
return (
|
|
94
158
|
<ChatCustomAction
|
|
95
159
|
content={message.content as string}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
TbAlertTriangle,
|
|
4
|
+
TbCheck,
|
|
5
|
+
TbChevronDown,
|
|
6
|
+
TbClock,
|
|
7
|
+
TbLoader2,
|
|
8
|
+
} from 'react-icons/tb'
|
|
9
|
+
import classNames from '@/components/ui/utils/classNames'
|
|
10
|
+
import {
|
|
11
|
+
camposVisiveis,
|
|
12
|
+
iconeFerramenta,
|
|
13
|
+
rotularExecucao,
|
|
14
|
+
rotularFerramenta,
|
|
15
|
+
} from './toolPresentation'
|
|
16
|
+
import type { ToolCall } from '../types'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Uma consulta ou operacao que o agente executou, no meio da conversa.
|
|
20
|
+
*
|
|
21
|
+
* Deliberadamente discreto: e informacao de apoio, nao a resposta. Por isso vive
|
|
22
|
+
* FORA do balao de fala (ver ChatView) e tem forma de pilula — contorno fino,
|
|
23
|
+
* largura do conteudo, letra menor — para nunca ser lido como uma segunda
|
|
24
|
+
* mensagem do agente. Fechado mostra so o que aconteceu e em que estado esta;
|
|
25
|
+
* aberto, os parametros ja traduzidos para a linguagem do negocio.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
type Estado = {
|
|
29
|
+
texto: string
|
|
30
|
+
Icone: typeof TbCheck
|
|
31
|
+
classe: string
|
|
32
|
+
/**
|
|
33
|
+
* Se o texto do estado acompanha o icone na pilula.
|
|
34
|
+
*
|
|
35
|
+
* So quando o icone sozinho nao basta: "aguardando confirmacao" e "nao
|
|
36
|
+
* concluida" pedem uma acao ou explicam uma falha. Em execucao o rotulo ja
|
|
37
|
+
* esta no gerundio ("Consultando contratos…") e o ✓ de concluida e obvio —
|
|
38
|
+
* escrever isso de novo so engorda a pilula.
|
|
39
|
+
*/
|
|
40
|
+
mostrarTexto?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const estadoDe = (tool: ToolCall): Estado => {
|
|
44
|
+
if (tool.awaitingApproval) {
|
|
45
|
+
return {
|
|
46
|
+
texto: 'Aguardando confirmação',
|
|
47
|
+
Icone: TbClock,
|
|
48
|
+
classe: 'text-amber-600 dark:text-amber-400',
|
|
49
|
+
mostrarTexto: true,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (tool.running) {
|
|
53
|
+
return {
|
|
54
|
+
texto: 'Em andamento',
|
|
55
|
+
Icone: TbLoader2,
|
|
56
|
+
classe: 'text-blue-600 dark:text-blue-400',
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (tool.isError) {
|
|
60
|
+
return {
|
|
61
|
+
texto: 'Não concluída',
|
|
62
|
+
Icone: TbAlertTriangle,
|
|
63
|
+
classe: 'text-red-600 dark:text-red-400',
|
|
64
|
+
mostrarTexto: true,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
texto: 'Concluída',
|
|
69
|
+
Icone: TbCheck,
|
|
70
|
+
classe: 'text-emerald-600 dark:text-emerald-400',
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type ToolCallChipProps = {
|
|
75
|
+
tool: ToolCall
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const ToolCallChip = ({ tool }: ToolCallChipProps) => {
|
|
79
|
+
const [aberto, setAberto] = useState(false)
|
|
80
|
+
const estado = estadoDe(tool)
|
|
81
|
+
const IconeFerramenta = iconeFerramenta(tool.name)
|
|
82
|
+
const emAndamento = tool.running && !tool.awaitingApproval
|
|
83
|
+
const campos = camposVisiveis(tool.args)
|
|
84
|
+
const rotulo = emAndamento
|
|
85
|
+
? `${rotularExecucao(tool.name)}…`
|
|
86
|
+
: rotularFerramenta(tool.name)
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
// not-prose: o balao do agente e `prose`, que aplicaria margens de
|
|
90
|
+
// tipografia no dl/dt/dd/pre daqui e desmontaria a compactacao.
|
|
91
|
+
<div className="not-prose max-w-full">
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
className="inline-flex w-fit max-w-full items-center gap-1.5 rounded-full border border-gray-200 px-2.5 py-1 text-left transition-colors hover:border-gray-300 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600 dark:hover:bg-gray-800/60"
|
|
95
|
+
aria-expanded={aberto}
|
|
96
|
+
title={`${rotulo} — ${estado.texto}`}
|
|
97
|
+
onClick={() => setAberto((atual) => !atual)}
|
|
98
|
+
>
|
|
99
|
+
<IconeFerramenta
|
|
100
|
+
size={13}
|
|
101
|
+
className="shrink-0 text-gray-400 dark:text-gray-500"
|
|
102
|
+
/>
|
|
103
|
+
|
|
104
|
+
<span className="truncate text-xs font-medium text-gray-600 dark:text-gray-300">
|
|
105
|
+
{rotulo}
|
|
106
|
+
</span>
|
|
107
|
+
|
|
108
|
+
<span
|
|
109
|
+
className={classNames(
|
|
110
|
+
'flex shrink-0 items-center gap-1 text-xs font-medium',
|
|
111
|
+
estado.classe,
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
<estado.Icone
|
|
115
|
+
size={12}
|
|
116
|
+
className={emAndamento ? 'motion-safe:animate-spin' : ''}
|
|
117
|
+
/>
|
|
118
|
+
{estado.mostrarTexto && estado.texto}
|
|
119
|
+
</span>
|
|
120
|
+
|
|
121
|
+
<TbChevronDown
|
|
122
|
+
size={12}
|
|
123
|
+
className={classNames(
|
|
124
|
+
'shrink-0 text-gray-400 transition-transform',
|
|
125
|
+
aberto && 'rotate-180',
|
|
126
|
+
)}
|
|
127
|
+
/>
|
|
128
|
+
</button>
|
|
129
|
+
|
|
130
|
+
{aberto && (
|
|
131
|
+
<div className="mt-1 rounded-lg border border-gray-200 px-2.5 py-2 dark:border-gray-700">
|
|
132
|
+
{campos.length > 0 && (
|
|
133
|
+
<dl className="grid grid-cols-[minmax(0,auto)_minmax(0,1fr)] gap-x-3 gap-y-1 text-xs">
|
|
134
|
+
{campos.map((campo) => (
|
|
135
|
+
<div key={campo.chave} className="contents">
|
|
136
|
+
<dt className="text-gray-500 dark:text-gray-400">
|
|
137
|
+
{campo.rotulo}
|
|
138
|
+
</dt>
|
|
139
|
+
<dd className="truncate font-medium text-gray-800 dark:text-gray-100">
|
|
140
|
+
{campo.valor}
|
|
141
|
+
</dd>
|
|
142
|
+
</div>
|
|
143
|
+
))}
|
|
144
|
+
</dl>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{tool.result !== undefined && (
|
|
148
|
+
<Resultado
|
|
149
|
+
texto={tool.result}
|
|
150
|
+
erro={tool.isError}
|
|
151
|
+
separar={campos.length > 0}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* O retorno da ferramenta.
|
|
162
|
+
*
|
|
163
|
+
* Quando e JSON (uma listagem, por exemplo), fica atras de um toggle: e dado
|
|
164
|
+
* tecnico que so interessa para conferencia, e o usuario ja tem a resposta do
|
|
165
|
+
* agente logo abaixo. Mensagens em texto — erros de permissao, cancelamentos —
|
|
166
|
+
* aparecem direto, porque explicam o que aconteceu.
|
|
167
|
+
*/
|
|
168
|
+
const Resultado = ({
|
|
169
|
+
texto,
|
|
170
|
+
erro,
|
|
171
|
+
separar,
|
|
172
|
+
}: {
|
|
173
|
+
texto: string
|
|
174
|
+
erro?: boolean
|
|
175
|
+
/** Linha divisoria: so faz sentido quando ha parametros logo acima. */
|
|
176
|
+
separar?: boolean
|
|
177
|
+
}) => {
|
|
178
|
+
const [mostrarBruto, setMostrarBruto] = useState(false)
|
|
179
|
+
const ehJson = /^\s*[[{]/.test(texto)
|
|
180
|
+
const separador = separar
|
|
181
|
+
? 'mt-2 border-t border-gray-200 pt-2 dark:border-gray-700'
|
|
182
|
+
: ''
|
|
183
|
+
|
|
184
|
+
if (!ehJson) {
|
|
185
|
+
return (
|
|
186
|
+
<p
|
|
187
|
+
className={classNames(
|
|
188
|
+
'text-xs',
|
|
189
|
+
separador,
|
|
190
|
+
erro
|
|
191
|
+
? 'text-red-600 dark:text-red-400'
|
|
192
|
+
: 'text-gray-600 dark:text-gray-300',
|
|
193
|
+
)}
|
|
194
|
+
>
|
|
195
|
+
{texto}
|
|
196
|
+
</p>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div className={separador}>
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
className="text-xs font-medium text-gray-500 underline-offset-2 hover:underline dark:text-gray-400"
|
|
205
|
+
onClick={() => setMostrarBruto((atual) => !atual)}
|
|
206
|
+
>
|
|
207
|
+
{mostrarBruto ? 'Ocultar' : 'Ver'} dados retornados
|
|
208
|
+
</button>
|
|
209
|
+
{mostrarBruto && (
|
|
210
|
+
<pre className="mt-1.5 max-h-40 overflow-auto whitespace-pre-wrap break-words rounded border border-gray-200 bg-white p-2 text-xs text-gray-600 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300">
|
|
211
|
+
{texto}
|
|
212
|
+
</pre>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export default ToolCallChip
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { formatQuantity } from '@base/utils/functions/stringUtils'
|
|
2
|
+
import { TbFilePlus, TbListSearch, TbSearch, TbTool } from 'react-icons/tb'
|
|
3
|
+
import type { IconType } from 'react-icons'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Como as ferramentas do agente sao apresentadas ao usuario.
|
|
7
|
+
*
|
|
8
|
+
* Compartilhado pelo chip de execucao e pelo card de aprovacao, para os dois
|
|
9
|
+
* falarem a mesma lingua: o usuario ve "Consulta de contratos" e "Mercado
|
|
10
|
+
* interno", nunca `listar_contratos_armazenagem_graos` ou `MERCADO_INTERNO`.
|
|
11
|
+
*
|
|
12
|
+
* Tudo tem fallback: uma ferramenta ou campo novo no backend nao quebra a tela,
|
|
13
|
+
* so aparece com o nome tecnico humanizado ate ganhar entrada aqui.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
type Ferramenta = {
|
|
17
|
+
rotulo: string
|
|
18
|
+
/** Frase no gerundio, para o estado "executando". */
|
|
19
|
+
executando: string
|
|
20
|
+
icone: IconType
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const FERRAMENTAS: Record<string, Ferramenta> = {
|
|
24
|
+
buscar_contrato_armazenagem_graos: {
|
|
25
|
+
rotulo: 'Consulta de contrato',
|
|
26
|
+
executando: 'Consultando contrato',
|
|
27
|
+
icone: TbSearch,
|
|
28
|
+
},
|
|
29
|
+
listar_contratos_armazenagem_graos: {
|
|
30
|
+
rotulo: 'Consulta de contratos',
|
|
31
|
+
executando: 'Consultando contratos',
|
|
32
|
+
icone: TbListSearch,
|
|
33
|
+
},
|
|
34
|
+
criar_contrato_armazenagem_graos: {
|
|
35
|
+
rotulo: 'Criação de contrato de armazenagem',
|
|
36
|
+
executando: 'Criando contrato',
|
|
37
|
+
icone: TbFilePlus,
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const humanizar = (texto: string) =>
|
|
42
|
+
texto
|
|
43
|
+
.replaceAll('_', ' ')
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/^./, (letra) => letra.toUpperCase())
|
|
46
|
+
|
|
47
|
+
export const rotularFerramenta = (nome: string) =>
|
|
48
|
+
FERRAMENTAS[nome]?.rotulo ?? humanizar(nome)
|
|
49
|
+
|
|
50
|
+
export const rotularExecucao = (nome: string) =>
|
|
51
|
+
FERRAMENTAS[nome]?.executando ?? humanizar(nome)
|
|
52
|
+
|
|
53
|
+
export const iconeFerramenta = (nome: string): IconType =>
|
|
54
|
+
FERRAMENTAS[nome]?.icone ?? TbTool
|
|
55
|
+
|
|
56
|
+
/** Rotulos dos parametros, na linguagem do negocio. */
|
|
57
|
+
const CAMPOS: Record<string, string> = {
|
|
58
|
+
business_partner_id: 'Parceiro de negócio',
|
|
59
|
+
produto_id: 'Produto',
|
|
60
|
+
safra_id: 'Safra',
|
|
61
|
+
saldo_negociado: 'Saldo negociado',
|
|
62
|
+
tipo_contrato: 'Tipo de contrato',
|
|
63
|
+
contrato_venda: 'Contrato de venda',
|
|
64
|
+
ordem_venda: 'Ordem de venda',
|
|
65
|
+
id: 'Contrato',
|
|
66
|
+
page_number: 'Página',
|
|
67
|
+
page_size: 'Itens por página',
|
|
68
|
+
sort_field: 'Ordenar por',
|
|
69
|
+
sort_order: 'Ordem',
|
|
70
|
+
filtro: 'Filtro',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const rotularCampo = (chave: string) => CAMPOS[chave] ?? humanizar(chave)
|
|
74
|
+
|
|
75
|
+
/** Campos que sao referencia a um registro: exibidos como "#123". */
|
|
76
|
+
const REFERENCIAS = new Set([
|
|
77
|
+
'business_partner_id',
|
|
78
|
+
'produto_id',
|
|
79
|
+
'safra_id',
|
|
80
|
+
'contrato_venda',
|
|
81
|
+
'ordem_venda',
|
|
82
|
+
'id',
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
const ORDENACAO: Record<string, string> = { ASC: 'Crescente', DESC: 'Decrescente' }
|
|
86
|
+
|
|
87
|
+
/** Converte o valor tecnico no que o usuario espera ler. */
|
|
88
|
+
export const formatarValor = (chave: string, valor: unknown): string => {
|
|
89
|
+
if (valor === null || valor === undefined || valor === '') {
|
|
90
|
+
return '—'
|
|
91
|
+
}
|
|
92
|
+
if (chave === 'saldo_negociado' && typeof valor === 'number') {
|
|
93
|
+
return formatQuantity(valor)
|
|
94
|
+
}
|
|
95
|
+
if (chave === 'sort_order' && typeof valor === 'string') {
|
|
96
|
+
return ORDENACAO[valor] ?? humanizar(valor)
|
|
97
|
+
}
|
|
98
|
+
if (typeof valor === 'number') {
|
|
99
|
+
return REFERENCIAS.has(chave) ? `#${valor}` : String(valor)
|
|
100
|
+
}
|
|
101
|
+
if (typeof valor === 'boolean') {
|
|
102
|
+
return valor ? 'Sim' : 'Não'
|
|
103
|
+
}
|
|
104
|
+
if (typeof valor === 'string') {
|
|
105
|
+
// Enums do backend chegam em CAIXA_ALTA_COM_UNDERSCORE.
|
|
106
|
+
return /^[A-Z][A-Z0-9_]+$/.test(valor) ? humanizar(valor) : valor
|
|
107
|
+
}
|
|
108
|
+
if (Array.isArray(valor)) {
|
|
109
|
+
return valor.length ? `${valor.length} item(ns)` : '—'
|
|
110
|
+
}
|
|
111
|
+
if (typeof valor === 'object') {
|
|
112
|
+
const entradas = Object.entries(valor as Record<string, unknown>)
|
|
113
|
+
if (!entradas.length) {
|
|
114
|
+
return '—'
|
|
115
|
+
}
|
|
116
|
+
return entradas
|
|
117
|
+
.map(([k, v]) => `${rotularCampo(k)}: ${formatarValor(k, v)}`)
|
|
118
|
+
.join(', ')
|
|
119
|
+
}
|
|
120
|
+
return String(valor)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Pares (rotulo, valor) prontos para exibir, ignorando o que esta vazio. */
|
|
124
|
+
export const camposVisiveis = (args: Record<string, unknown>) =>
|
|
125
|
+
Object.entries(args)
|
|
126
|
+
.filter(([, valor]) => {
|
|
127
|
+
if (valor === null || valor === undefined || valor === '') return false
|
|
128
|
+
if (typeof valor === 'object' && !Array.isArray(valor)) {
|
|
129
|
+
return Object.keys(valor as object).length > 0
|
|
130
|
+
}
|
|
131
|
+
return true
|
|
132
|
+
})
|
|
133
|
+
.map(([chave, valor]) => ({
|
|
134
|
+
chave,
|
|
135
|
+
rotulo: rotularCampo(chave),
|
|
136
|
+
valor: formatarValor(chave, valor),
|
|
137
|
+
}))
|