@adatechnology/conversations-ui 0.1.0-rc.21 → 0.1.0-rc.23
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/index.d.ts +237 -3
- package/dist/index.js +854 -0
- package/dist/preview/index.d.ts +23 -1
- package/dist/preview/index.js +48 -14
- package/dist/styles.css +559 -0
- package/package.json +1 -1
- package/src/index.ts +15 -0
- package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
- package/src/preview/ConversationSimulatorPanel.tsx +89 -0
- package/src/preview/createPreviewWebhookClient.test.ts +11 -4
- package/src/preview/index.ts +3 -0
- package/src/styles.css +542 -0
- package/src/workspace/BulkTemplateModal.tsx +130 -0
- package/src/workspace/ConversationPane.tsx +287 -0
- package/src/workspace/ConversationsInboxList.tsx +185 -0
- package/src/workspace/ConversationsWorkspace.tsx +306 -0
- package/src/workspace/index.ts +12 -0
- package/src/workspace/labels.test.ts +17 -0
- package/src/workspace/labels.ts +80 -0
- package/src/workspace/useConversationsInbox.ts +329 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -209,3 +209,18 @@ export { createMediaUrlResolver } from './lib/createMediaUrlResolver'
|
|
|
209
209
|
export type { ConversationHeaderUtility } from './ConversationHeader'
|
|
210
210
|
export { applyQuickReplyVariables, resolveQuickReply } from './MessageComposer'
|
|
211
211
|
export type { QuickReply } from './MessageComposer'
|
|
212
|
+
|
|
213
|
+
// Tela de atendimento completa. Fica no export principal — e não num subpath — porque é a
|
|
214
|
+
// composição padrão do pacote: quem instala conversas quer esta tela, e as peças continuam
|
|
215
|
+
// exportadas ao lado para quem precisar montar outra.
|
|
216
|
+
export { ConversationsWorkspace, ConversationPane, ConversationsInboxList } from './workspace'
|
|
217
|
+
export { useConversationsInbox, CONVERSATIONS_PER_PAGE, DEFAULT_CONVERSATIONS_WORKSPACE_LABELS } from './workspace'
|
|
218
|
+
export type {
|
|
219
|
+
ConversationsWorkspaceProps,
|
|
220
|
+
ConversationsWorkspaceSimulator,
|
|
221
|
+
ConversationsWorkspaceLabels,
|
|
222
|
+
ConversationPaneProps,
|
|
223
|
+
ConversationsInboxListProps,
|
|
224
|
+
UseConversationsInboxParams,
|
|
225
|
+
UseConversationsInboxResult,
|
|
226
|
+
} from './workspace'
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
|
|
4
|
+
import { ConversationSimulatorPanel, type ConversationSimulatorPanelProps } from './ConversationSimulatorPanel'
|
|
5
|
+
import { createMockSSEProvider } from './createMockSSEProvider'
|
|
6
|
+
import { createPreviewStore } from './previewStore'
|
|
7
|
+
|
|
8
|
+
function markupOf(overrides: Partial<ConversationSimulatorPanelProps> = {}): string {
|
|
9
|
+
const store = createPreviewStore({ conversations: [], messages: {} })
|
|
10
|
+
|
|
11
|
+
return renderToStaticMarkup(
|
|
12
|
+
<ConversationSimulatorPanel
|
|
13
|
+
conversationId="5511900000042"
|
|
14
|
+
onClose={() => {}}
|
|
15
|
+
client={{ sendText: async () => {}, sendInteractiveReply: async () => {} } as never}
|
|
16
|
+
sse={createMockSSEProvider({ store })}
|
|
17
|
+
loadMessages={async () => []}
|
|
18
|
+
{...overrides}
|
|
19
|
+
/>,
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('ConversationSimulatorPanel', () => {
|
|
24
|
+
it('mostra o telefone formatado pelo host, e não o id cru', () => {
|
|
25
|
+
expect(markupOf({ displayNumber: '+55 (11) 90000-0042' })).toContain('+55 (11) 90000-0042')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('cai no id da conversa quando o host não formata', () => {
|
|
29
|
+
expect(markupOf()).toContain('5511900000042')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('avisa para onde a mensagem vai, para ninguém achar que é conversa de mentira', () => {
|
|
33
|
+
expect(markupOf()).toContain('entrega no webhook real')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('dá nome acessível ao botão de fechar, que só tem ícone', () => {
|
|
37
|
+
expect(markupOf()).toContain('aria-label="Fechar simulador"')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('aceita rótulos parciais do host sem exigir o conjunto inteiro', () => {
|
|
41
|
+
const markup = markupOf({ labels: { title: 'Testar fluxo' } })
|
|
42
|
+
|
|
43
|
+
expect(markup).toContain('Testar fluxo')
|
|
44
|
+
// O que não foi sobrescrito continua no default.
|
|
45
|
+
expect(markup).toContain('aria-label="Fechar simulador"')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('renderiza ação extra do host no cabeçalho', () => {
|
|
49
|
+
expect(markupOf({ headerActions: <button type="button">Rodar roteiro</button> })).toContain('Rodar roteiro')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('nomeia o próprio aside, para o leitor de tela distinguir do transcript ao lado', () => {
|
|
53
|
+
expect(markupOf()).toContain('aria-label="Simulador do cliente"')
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 Ada Technology. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* This source code is proprietary and confidential. Unauthorized copying,
|
|
5
|
+
* modification, distribution, or use of this file, via any medium, is
|
|
6
|
+
* strictly prohibited without prior written permission from Ada Technology.
|
|
7
|
+
*
|
|
8
|
+
* Author: Anderson Filho <andersonfrfilho@gmail.com>
|
|
9
|
+
*
|
|
10
|
+
* Painel lateral do simulador de cliente: a moldura em volta do `ConversationPreview`.
|
|
11
|
+
*
|
|
12
|
+
* Existe como componente do pacote porque cada produto tinha reescrito a mesma moldura —
|
|
13
|
+
* `<aside>`, cabeçalho com título e telefone, botão de fechar — e as cópias divergiram. Uma delas
|
|
14
|
+
* migrou para o cliente-ponte (assinatura no servidor) e a outra ficou assinando no navegador, com
|
|
15
|
+
* o app secret publicado no bundle. É o tipo de correção que precisa chegar a todo mundo de uma vez.
|
|
16
|
+
*
|
|
17
|
+
* Mora DENTRO da tela de conversas, e não numa aba própria, por um motivo prático: o token do
|
|
18
|
+
* atendente costuma viver em `sessionStorage`, que é por aba. Numa aba separada o simulador nascia
|
|
19
|
+
* sem sessão, o transcript nunca carregava, e o sintoma era "mandei e não aconteceu nada".
|
|
20
|
+
*
|
|
21
|
+
* O efeito de cada envio aparece na thread ao lado pelo mesmo stream que a inbox já assina — este
|
|
22
|
+
* painel não abre conexão própria.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { ReactNode } from 'react'
|
|
26
|
+
|
|
27
|
+
import { ConversationPreview, type ConversationPreviewProps } from './ConversationPreview'
|
|
28
|
+
|
|
29
|
+
export type ConversationSimulatorPanelLabels = {
|
|
30
|
+
readonly title: string
|
|
31
|
+
/** Complementa o telefone no subtítulo, explicando para onde a mensagem realmente vai. */
|
|
32
|
+
readonly destinationHint: string
|
|
33
|
+
readonly close: string
|
|
34
|
+
readonly placeholder: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS: ConversationSimulatorPanelLabels = {
|
|
38
|
+
title: 'Simulador do cliente',
|
|
39
|
+
destinationHint: 'entrega no webhook real',
|
|
40
|
+
close: 'Fechar simulador',
|
|
41
|
+
placeholder: 'Escreva como o cliente…',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type ConversationSimulatorPanelProps = Omit<ConversationPreviewProps, 'placeholder'> & {
|
|
45
|
+
readonly onClose: () => void
|
|
46
|
+
/**
|
|
47
|
+
* Telefone já formatado para leitura. É o host que formata: máscara de telefone é convenção
|
|
48
|
+
* regional, e o pacote não tem como saber a do produto.
|
|
49
|
+
*/
|
|
50
|
+
readonly displayNumber?: string
|
|
51
|
+
readonly labels?: Partial<ConversationSimulatorPanelLabels>
|
|
52
|
+
/** Ações extras no cabeçalho — roteiro automático, limpar conversa, trocar de contato. */
|
|
53
|
+
readonly headerActions?: ReactNode
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function ConversationSimulatorPanel({
|
|
57
|
+
onClose,
|
|
58
|
+
displayNumber,
|
|
59
|
+
labels,
|
|
60
|
+
headerActions,
|
|
61
|
+
...previewProps
|
|
62
|
+
}: ConversationSimulatorPanelProps) {
|
|
63
|
+
const text = { ...DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS, ...labels }
|
|
64
|
+
const subtitle = [displayNumber ?? previewProps.conversationId, text.destinationHint].join(' · ')
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<aside className="cv-simulator-panel" aria-label={text.title}>
|
|
68
|
+
<header className="cv-simulator-panel__header">
|
|
69
|
+
<div className="cv-simulator-panel__heading">
|
|
70
|
+
<h2 className="cv-simulator-panel__title">{text.title}</h2>
|
|
71
|
+
<p className="cv-simulator-panel__subtitle">{subtitle}</p>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="cv-simulator-panel__actions">
|
|
74
|
+
{headerActions}
|
|
75
|
+
<button type="button" onClick={onClose} title={text.close} aria-label={text.close} className="cv-simulator-panel__close">
|
|
76
|
+
{/* SVG inline em vez de lucide-react: o pacote não impõe biblioteca de ícone ao host. */}
|
|
77
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
|
78
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
79
|
+
</svg>
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</header>
|
|
83
|
+
|
|
84
|
+
<div className="cv-simulator-panel__body">
|
|
85
|
+
<ConversationPreview {...previewProps} placeholder={text.placeholder} />
|
|
86
|
+
</div>
|
|
87
|
+
</aside>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -36,6 +36,13 @@ function createCapturingFetch(status = 200): { fetchImplementation: typeof fetch
|
|
|
36
36
|
return { fetchImplementation, captured }
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/** `uploadMedia` é opcional no contrato — o cliente do webhook sempre traz, o teste garante isso. */
|
|
40
|
+
function uploadMediaOf(client: { uploadMedia?: (file: File) => Promise<unknown> }) {
|
|
41
|
+
const upload = client.uploadMedia
|
|
42
|
+
if (!upload) throw new Error('createPreviewWebhookClient deveria expor uploadMedia')
|
|
43
|
+
return upload
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
describe('createPreviewWebhookClient', () => {
|
|
40
47
|
it('assina com o mesmo HMAC que o servidor calcula em node:crypto', async () => {
|
|
41
48
|
const { fetchImplementation, captured } = createCapturingFetch()
|
|
@@ -144,7 +151,7 @@ describe('createPreviewWebhookClient.uploadMedia', () => {
|
|
|
144
151
|
fetchImplementation,
|
|
145
152
|
})
|
|
146
153
|
|
|
147
|
-
const uploaded = await client
|
|
154
|
+
const uploaded = (await uploadMediaOf(client)(audioFile())) as { mediaId: string; mimeType?: string }
|
|
148
155
|
|
|
149
156
|
expect(calls[0]?.url).toBe('http://localhost:3000/v1/preview/media')
|
|
150
157
|
expect(uploaded.mediaId).toBe(`${PREVIEW_MEDIA_ID_PREFIX}upl_123`)
|
|
@@ -160,7 +167,7 @@ describe('createPreviewWebhookClient.uploadMedia', () => {
|
|
|
160
167
|
fetchImplementation,
|
|
161
168
|
})
|
|
162
169
|
|
|
163
|
-
await client
|
|
170
|
+
await uploadMediaOf(client)(audioFile())
|
|
164
171
|
|
|
165
172
|
const expected = `sha256=${createHmac('sha256', APP_SECRET).update('audio/ogg').digest('hex')}`
|
|
166
173
|
expect(calls[0]?.signature).toBe(expected)
|
|
@@ -175,7 +182,7 @@ describe('createPreviewWebhookClient.uploadMedia', () => {
|
|
|
175
182
|
fetchImplementation,
|
|
176
183
|
})
|
|
177
184
|
|
|
178
|
-
await client
|
|
185
|
+
await uploadMediaOf(client)(audioFile())
|
|
179
186
|
|
|
180
187
|
expect(calls[0]?.url).toBe('/v1/preview/media')
|
|
181
188
|
})
|
|
@@ -189,6 +196,6 @@ describe('createPreviewWebhookClient.uploadMedia', () => {
|
|
|
189
196
|
fetchImplementation,
|
|
190
197
|
})
|
|
191
198
|
|
|
192
|
-
await expect(client
|
|
199
|
+
await expect(uploadMediaOf(client)(audioFile())).rejects.toBeInstanceOf(PreviewMediaUploadRejectedError)
|
|
193
200
|
})
|
|
194
201
|
})
|
package/src/preview/index.ts
CHANGED
|
@@ -25,6 +25,9 @@ export type { CreateMockSSEProviderParams } from './createMockSSEProvider'
|
|
|
25
25
|
|
|
26
26
|
export { PREVIEW_CONVERSATIONS, PREVIEW_MESSAGES, PREVIEW_DOCUMENTS } from './previewFixtures'
|
|
27
27
|
|
|
28
|
+
export { ConversationSimulatorPanel, DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS } from './ConversationSimulatorPanel'
|
|
29
|
+
export type { ConversationSimulatorPanelProps, ConversationSimulatorPanelLabels } from './ConversationSimulatorPanel'
|
|
30
|
+
|
|
28
31
|
export { ConversationPreview } from './ConversationPreview'
|
|
29
32
|
export { mediaTypeOf } from './ConversationPreview'
|
|
30
33
|
export type { ConversationPreviewProps, PreviewUploadedMedia } from './ConversationPreview'
|