@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.
Files changed (93) hide show
  1. package/dist/@ecme/assets/styles/app.css +116 -3
  2. package/dist/@ecme/assets/styles/tailwind/index.css +119 -3
  3. package/dist/@ecme/components/template/Footer.js +1 -1
  4. package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.js +0 -2
  5. package/dist/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.js.map +1 -1
  6. package/dist/@ecme/components/view/ChatBox/ChatBox.js +2 -0
  7. package/dist/@ecme/components/view/ChatBox/ChatBox.js.map +1 -1
  8. package/dist/@ecme/components/view/ChatBox/components/MessageList.js +2 -1
  9. package/dist/@ecme/components/view/ChatBox/components/MessageList.js.map +1 -1
  10. package/dist/@ecme/index.js +3 -4
  11. package/dist/@ecme/services/AiService.js +53 -24
  12. package/dist/@ecme/services/AiService.js.map +1 -1
  13. package/dist/@ecme/services/aiAuthHeaders.js +35 -0
  14. package/dist/@ecme/services/aiAuthHeaders.js.map +1 -0
  15. package/dist/@ecme/services/axios/AxiosRequestIntrceptorConfigCallback.js +4 -27
  16. package/dist/@ecme/services/axios/AxiosRequestIntrceptorConfigCallback.js.map +1 -1
  17. package/dist/@ecme/services/index.js +4 -5
  18. package/dist/@ecme/services/sseParser.js +42 -0
  19. package/dist/@ecme/services/sseParser.js.map +1 -0
  20. package/dist/@ecme/views/concepts/ai/Chat/components/AiErrorMessage.js +15 -0
  21. package/dist/@ecme/views/concepts/ai/Chat/components/AiErrorMessage.js.map +1 -0
  22. package/dist/@ecme/views/concepts/ai/Chat/components/ApprovalCard.js +73 -0
  23. package/dist/@ecme/views/concepts/ai/Chat/components/ApprovalCard.js.map +1 -0
  24. package/dist/@ecme/views/concepts/ai/Chat/components/ChatView.js +37 -10
  25. package/dist/@ecme/views/concepts/ai/Chat/components/ChatView.js.map +1 -1
  26. package/dist/@ecme/views/concepts/ai/Chat/components/ToolCallChip.js +153 -0
  27. package/dist/@ecme/views/concepts/ai/Chat/components/ToolCallChip.js.map +1 -0
  28. package/dist/@ecme/views/concepts/ai/Chat/components/toolPresentation.js +99 -0
  29. package/dist/@ecme/views/concepts/ai/Chat/components/toolPresentation.js.map +1 -0
  30. package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatSend.js +111 -17
  31. package/dist/@ecme/views/concepts/ai/Chat/hooks/useChatSend.js.map +1 -1
  32. package/dist/@ecme/views/concepts/ai/Chat/store/generativeChatStore.js +29 -13
  33. package/dist/@ecme/views/concepts/ai/Chat/store/generativeChatStore.js.map +1 -1
  34. package/dist/@types/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.d.ts.map +1 -1
  35. package/dist/@types/@ecme/components/view/ChatBox/ChatBox.d.ts.map +1 -1
  36. package/dist/@types/@ecme/components/view/ChatBox/components/MessageList.d.ts +2 -0
  37. package/dist/@types/@ecme/components/view/ChatBox/components/MessageList.d.ts.map +1 -1
  38. package/dist/@types/@ecme/services/AiService.d.ts +28 -15
  39. package/dist/@types/@ecme/services/AiService.d.ts.map +1 -1
  40. package/dist/@types/@ecme/services/aiAuthHeaders.d.ts +12 -0
  41. package/dist/@types/@ecme/services/aiAuthHeaders.d.ts.map +1 -0
  42. package/dist/@types/@ecme/services/axios/AxiosRequestIntrceptorConfigCallback.d.ts +6 -0
  43. package/dist/@types/@ecme/services/axios/AxiosRequestIntrceptorConfigCallback.d.ts.map +1 -1
  44. package/dist/@types/@ecme/services/sseParser.d.ts +20 -0
  45. package/dist/@types/@ecme/services/sseParser.d.ts.map +1 -0
  46. package/dist/@types/@ecme/views/concepts/ai/Chat/components/AiErrorMessage.d.ts +8 -0
  47. package/dist/@types/@ecme/views/concepts/ai/Chat/components/AiErrorMessage.d.ts.map +1 -0
  48. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ApprovalCard.d.ts +15 -0
  49. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ApprovalCard.d.ts.map +1 -0
  50. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ChatView.d.ts.map +1 -1
  51. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ToolCallChip.d.ts +7 -0
  52. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ToolCallChip.d.ts.map +1 -0
  53. package/dist/@types/@ecme/views/concepts/ai/Chat/components/toolPresentation.d.ts +14 -0
  54. package/dist/@types/@ecme/views/concepts/ai/Chat/components/toolPresentation.d.ts.map +1 -0
  55. package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useChatSend.d.ts +2 -1
  56. package/dist/@types/@ecme/views/concepts/ai/Chat/hooks/useChatSend.d.ts.map +1 -1
  57. package/dist/@types/@ecme/views/concepts/ai/Chat/store/generativeChatStore.d.ts +1 -0
  58. package/dist/@types/@ecme/views/concepts/ai/Chat/store/generativeChatStore.d.ts.map +1 -1
  59. package/dist/@types/@ecme/views/concepts/ai/Chat/types.d.ts +96 -23
  60. package/dist/@types/@ecme/views/concepts/ai/Chat/types.d.ts.map +1 -1
  61. package/dist/@types/base/@types/models/transportadora/irpf.d.ts +1 -0
  62. package/dist/@types/base/@types/models/transportadora/irpf.d.ts.map +1 -1
  63. package/dist/@types/base/@types/models/transportadora/irpfFaixaDesconto.d.ts +0 -1
  64. package/dist/@types/base/@types/models/transportadora/irpfFaixaDesconto.d.ts.map +1 -1
  65. package/dist/base/assets/styles/init.css +128 -3
  66. package/lib/@ecme/components/template/OrbeAiWidget/OrbeAiWidget.tsx +0 -2
  67. package/lib/@ecme/components/view/ChatBox/ChatBox.tsx +2 -0
  68. package/lib/@ecme/components/view/ChatBox/components/MessageList.tsx +8 -4
  69. package/lib/@ecme/services/AiService.ts +89 -44
  70. package/lib/@ecme/services/aiAuthHeaders.ts +58 -0
  71. package/lib/@ecme/services/axios/AxiosRequestIntrceptorConfigCallback.ts +10 -39
  72. package/lib/@ecme/services/sseParser.ts +75 -0
  73. package/lib/@ecme/views/concepts/ai/Chat/components/AiErrorMessage.tsx +28 -0
  74. package/lib/@ecme/views/concepts/ai/Chat/components/ApprovalCard.tsx +103 -0
  75. package/lib/@ecme/views/concepts/ai/Chat/components/ChatView.tsx +74 -10
  76. package/lib/@ecme/views/concepts/ai/Chat/components/ToolCallChip.tsx +218 -0
  77. package/lib/@ecme/views/concepts/ai/Chat/components/toolPresentation.ts +137 -0
  78. package/lib/@ecme/views/concepts/ai/Chat/hooks/useChatSend.ts +175 -23
  79. package/lib/@ecme/views/concepts/ai/Chat/store/generativeChatStore.ts +41 -13
  80. package/lib/@ecme/views/concepts/ai/Chat/types.ts +82 -29
  81. package/lib/base/@types/models/transportadora/irpf.ts +1 -0
  82. package/lib/base/@types/models/transportadora/irpfFaixaDesconto.ts +0 -1
  83. package/package.json +1 -1
  84. package/dist/@ecme/views/concepts/ai/Chat/components/ModelSelect.js +0 -48
  85. package/dist/@ecme/views/concepts/ai/Chat/components/ModelSelect.js.map +0 -1
  86. package/dist/@ecme/views/concepts/ai/Chat/store/aiModelStore.js +0 -15
  87. package/dist/@ecme/views/concepts/ai/Chat/store/aiModelStore.js.map +0 -1
  88. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ModelSelect.d.ts +0 -8
  89. package/dist/@types/@ecme/views/concepts/ai/Chat/components/ModelSelect.d.ts.map +0 -1
  90. package/dist/@types/@ecme/views/concepts/ai/Chat/store/aiModelStore.d.ts +0 -25
  91. package/dist/@types/@ecme/views/concepts/ai/Chat/store/aiModelStore.d.ts.map +0 -1
  92. package/lib/@ecme/views/concepts/ai/Chat/components/ModelSelect.tsx +0 -66
  93. 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
- renderTyping={() => <AiThinkingIndicator />}
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
- <ChatCustomContent
80
- content={message.content as string}
81
- triggerTyping={
82
- message.fresh ? message.fresh : false
83
- }
84
- onFinish={() => handleFinish(message.id)}
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
+ }))