@adatechnology/conversations-ui 0.3.2 → 0.4.0
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/{ConversationSimulatorPanel-DmV8uaFE.d.ts → ConversationSimulatorPanel-DnXElfY8.d.ts} +87 -73
- package/dist/{chunk-L4OE4ORQ.js → chunk-4P5KMWOI.js} +219 -161
- package/dist/index.d.ts +44 -6
- package/dist/index.js +16 -5
- package/dist/preview/index.d.ts +2 -2
- package/dist/preview/index.js +1 -1
- package/package.json +3 -2
- package/src/AudioRecorderButton.test.tsx +18 -0
- package/src/AudioRecorderButton.tsx +17 -2
- package/src/ChannelIcon.tsx +11 -0
- package/src/MessageBubble.test.tsx +33 -0
- package/src/MessageBubble.tsx +18 -5
- package/src/MessageComposer.test.tsx +39 -1
- package/src/MessageComposer.tsx +66 -15
- package/src/channelCapability.test.ts +47 -0
- package/src/channelCapability.ts +37 -0
- package/src/conversationChannel.ts +19 -10
- package/src/conversationWindow.ts +7 -5
- package/src/index.ts +5 -1
- package/src/preview/ConversationSimulatorPanel.tsx +4 -0
- package/src/workspace/ConversationPane.tsx +2 -0
|
@@ -17,6 +17,7 @@ export const CONVERSATION_CHANNEL = {
|
|
|
17
17
|
MESSENGER: 'messenger',
|
|
18
18
|
INSTAGRAM: 'instagram',
|
|
19
19
|
WEBCHAT: 'webchat',
|
|
20
|
+
EMAIL: 'email',
|
|
20
21
|
} as const
|
|
21
22
|
export type ConversationChannel = (typeof CONVERSATION_CHANNEL)[keyof typeof CONVERSATION_CHANNEL]
|
|
22
23
|
|
|
@@ -35,14 +36,21 @@ export const HANDLE_KIND = {
|
|
|
35
36
|
PHONE: 'phone',
|
|
36
37
|
USERNAME: 'username',
|
|
37
38
|
SESSION: 'session',
|
|
39
|
+
EMAIL: 'email',
|
|
38
40
|
} as const
|
|
39
41
|
export type HandleKind = (typeof HANDLE_KIND)[keyof typeof HANDLE_KIND]
|
|
40
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Só o que é **exibição** do canal — rótulo, ícone, como reabrir, como mostrar o identificador.
|
|
45
|
+
* Capacidade de **comportamento** (confirma leitura, tem janela e de quantas horas, aceita anexo e
|
|
46
|
+
* áudio, precisa de transporte próprio) não mora mais aqui: para canal que
|
|
47
|
+
* `@adatechnology/conversation-contracts` conhece, ela vem de `channelCapabilityFor` (RF10). Manter
|
|
48
|
+
* os dois tipos de capacidade juntos foi o que criou a "janela" duplicada (`windowHours`) que a
|
|
49
|
+
* spec 211 mandou apagar — a mesma verdade não pode morar em dois lugares.
|
|
50
|
+
*/
|
|
41
51
|
export type ChannelCapabilities = {
|
|
42
52
|
readonly label: string
|
|
43
53
|
readonly icon: string
|
|
44
|
-
readonly hasSessionWindow: boolean
|
|
45
|
-
readonly windowHours: number
|
|
46
54
|
readonly reopenMechanism: ReopenMechanism
|
|
47
55
|
readonly handleKind: HandleKind
|
|
48
56
|
}
|
|
@@ -51,8 +59,6 @@ export const CHANNEL_CAPABILITIES: Readonly<Record<ConversationChannel, ChannelC
|
|
|
51
59
|
[CONVERSATION_CHANNEL.WHATSAPP]: {
|
|
52
60
|
label: 'WhatsApp',
|
|
53
61
|
icon: '💬',
|
|
54
|
-
hasSessionWindow: true,
|
|
55
|
-
windowHours: 24,
|
|
56
62
|
reopenMechanism: REOPEN_MECHANISM.TEMPLATE,
|
|
57
63
|
handleKind: HANDLE_KIND.PHONE,
|
|
58
64
|
},
|
|
@@ -60,16 +66,12 @@ export const CHANNEL_CAPABILITIES: Readonly<Record<ConversationChannel, ChannelC
|
|
|
60
66
|
// Messenger também tem 24h, mas reabre com message tag — não com template aprovado.
|
|
61
67
|
label: 'Messenger',
|
|
62
68
|
icon: '📨',
|
|
63
|
-
hasSessionWindow: true,
|
|
64
|
-
windowHours: 24,
|
|
65
69
|
reopenMechanism: REOPEN_MECHANISM.TAG,
|
|
66
70
|
handleKind: HANDLE_KIND.USERNAME,
|
|
67
71
|
},
|
|
68
72
|
[CONVERSATION_CHANNEL.INSTAGRAM]: {
|
|
69
73
|
label: 'Instagram',
|
|
70
74
|
icon: '📷',
|
|
71
|
-
hasSessionWindow: true,
|
|
72
|
-
windowHours: 24,
|
|
73
75
|
reopenMechanism: REOPEN_MECHANISM.TAG,
|
|
74
76
|
handleKind: HANDLE_KIND.USERNAME,
|
|
75
77
|
},
|
|
@@ -77,11 +79,16 @@ export const CHANNEL_CAPABILITIES: Readonly<Record<ConversationChannel, ChannelC
|
|
|
77
79
|
// Chat próprio: sem intermediário, sem janela. Bloquear o composer aqui seria inventar limite.
|
|
78
80
|
label: 'Chat do site',
|
|
79
81
|
icon: '🌐',
|
|
80
|
-
hasSessionWindow: false,
|
|
81
|
-
windowHours: 0,
|
|
82
82
|
reopenMechanism: REOPEN_MECHANISM.NONE,
|
|
83
83
|
handleKind: HANDLE_KIND.SESSION,
|
|
84
84
|
},
|
|
85
|
+
[CONVERSATION_CHANNEL.EMAIL]: {
|
|
86
|
+
// RF10: e-mail entra no vocabulário; ele não reabre por template nem por tag — é só resposta.
|
|
87
|
+
label: 'E-mail',
|
|
88
|
+
icon: '✉️',
|
|
89
|
+
reopenMechanism: REOPEN_MECHANISM.NONE,
|
|
90
|
+
handleKind: HANDLE_KIND.EMAIL,
|
|
91
|
+
},
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
export function capabilitiesOf(channel: ConversationChannel | undefined): ChannelCapabilities {
|
|
@@ -135,6 +142,8 @@ export function formatContactHandle(params: FormatContactHandleParams): string {
|
|
|
135
142
|
|
|
136
143
|
if (handleKind === HANDLE_KIND.PHONE) return formatPhone(params.handle)
|
|
137
144
|
if (handleKind === HANDLE_KIND.USERNAME) return params.handle.startsWith('@') ? params.handle : `@${params.handle}`
|
|
145
|
+
// Endereço de e-mail já é o identificador exibível — nem telefone, nem "@usuário".
|
|
146
|
+
if (handleKind === HANDLE_KIND.EMAIL) return params.handle
|
|
138
147
|
|
|
139
148
|
// Sessão de chat de site é um id opaco: mostrar o hash inteiro não ajuda ninguém.
|
|
140
149
|
return `Visitante ${params.handle.slice(-6)}`
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Janela de sessão: o intervalo em que o canal aceita mensagem livre do atendente. No WhatsApp são
|
|
3
3
|
* 24h desde o último contato do cliente; fora dela só template. Cada canal tem a sua regra — e há
|
|
4
|
-
* canal sem janela nenhuma — então a política vem de `
|
|
4
|
+
* canal sem janela nenhuma — então a política vem de `channelCapabilityFor` (RF10), não de
|
|
5
|
+
* constante fixa.
|
|
5
6
|
*
|
|
6
7
|
* Mora no SDK porque é regra de plataforma, não de produto: todo projeto que usa este pacote
|
|
7
8
|
* precisa dela para saber o que o atendente ainda consegue fazer.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
|
-
import {
|
|
11
|
+
import { channelCapabilityFor } from './channelCapability'
|
|
12
|
+
import type { ConversationChannel } from './conversationChannel'
|
|
11
13
|
|
|
12
14
|
const MINUTE_MS = 60_000
|
|
13
15
|
const HOUR_MS = 60 * MINUTE_MS
|
|
@@ -52,12 +54,12 @@ export type WindowOfParams = {
|
|
|
52
54
|
* bloquearia o composer inventando um limite que a plataforma não impõe.
|
|
53
55
|
*/
|
|
54
56
|
export function windowOf(params: WindowOfParams): ConversationWindow {
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
57
|
+
const capability = channelCapabilityFor(params.channel)
|
|
58
|
+
if (capability.sessionWindowHours === null) return CONVERSATION_WINDOW.FRESH
|
|
57
59
|
|
|
58
60
|
if (!params.lastInboundAt) return CONVERSATION_WINDOW.EXPIRED
|
|
59
61
|
|
|
60
|
-
const windowMs =
|
|
62
|
+
const windowMs = capability.sessionWindowHours * HOUR_MS
|
|
61
63
|
const elapsed = params.now - new Date(params.lastInboundAt).getTime()
|
|
62
64
|
|
|
63
65
|
if (elapsed >= windowMs) return CONVERSATION_WINDOW.EXPIRED
|
package/src/index.ts
CHANGED
|
@@ -53,7 +53,7 @@ export {
|
|
|
53
53
|
isReplyOverdue,
|
|
54
54
|
} from './replyLatency'
|
|
55
55
|
export type { ReplyLatency, ReplyLatencyParams } from './replyLatency'
|
|
56
|
-
// Canal de origem:
|
|
56
|
+
// Canal de origem: exibição por plataforma (rótulo, ícone, reabertura, tipo de identificador).
|
|
57
57
|
export {
|
|
58
58
|
CONVERSATION_CHANNEL,
|
|
59
59
|
DEFAULT_CONVERSATION_CHANNEL,
|
|
@@ -75,6 +75,10 @@ export type {
|
|
|
75
75
|
HandleKind,
|
|
76
76
|
FormatContactHandleParams,
|
|
77
77
|
} from './conversationChannel'
|
|
78
|
+
// Capacidade comportamental do canal (RF10, D3) — confirma leitura, janela, anexo, áudio, resposta
|
|
79
|
+
// rápida, transporte próprio. Vem de `@adatechnology/conversation-contracts`; ver channelCapability.ts.
|
|
80
|
+
export { channelCapabilityFor } from './channelCapability'
|
|
81
|
+
export type { ChannelCapability } from '@adatechnology/conversation-contracts'
|
|
78
82
|
export type { ConversationWindow } from './conversationWindow'
|
|
79
83
|
export { ConversationRow } from './ConversationRow'
|
|
80
84
|
export { ChannelIcon, CHANNEL_BRAND_COLOR } from './ChannelIcon'
|
|
@@ -66,6 +66,10 @@ const SIMULATOR_PANEL_CHANNEL_WORDING: Readonly<Record<ConversationChannel, Chan
|
|
|
66
66
|
destinationHint: 'entrega na API do chat do site',
|
|
67
67
|
placeholder: 'Escreva como o visitante…',
|
|
68
68
|
},
|
|
69
|
+
[CONVERSATION_CHANNEL.EMAIL]: {
|
|
70
|
+
destinationHint: 'entrega na caixa de entrada do contratante',
|
|
71
|
+
placeholder: 'Escreva como quem respondeu o e-mail…',
|
|
72
|
+
},
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
/** Rótulos do painel já resolvidos para o canal — útil para o host que monta o cabeçalho por fora. */
|
|
@@ -389,6 +389,7 @@ export function ConversationPane({
|
|
|
389
389
|
<MessageBubble
|
|
390
390
|
message={message}
|
|
391
391
|
isMine={message.direction === 'outbound'}
|
|
392
|
+
channel={conversation.channel}
|
|
392
393
|
isFirstInGroup={!previous || previous.sender !== message.sender}
|
|
393
394
|
{...(messageSelection
|
|
394
395
|
? {
|
|
@@ -507,6 +508,7 @@ export function ConversationPane({
|
|
|
507
508
|
value={draft}
|
|
508
509
|
onChange={setDraft}
|
|
509
510
|
onSend={(text) => void handleSend(text)}
|
|
511
|
+
channel={conversation.channel}
|
|
510
512
|
// Habilita clipe E microfone: o composer desenha o gravador sozinho quando existe um jeito
|
|
511
513
|
// de entregar arquivo, porque áudio gravado é um anexo como qualquer outro.
|
|
512
514
|
{...(onAttach ? { onAttach: (file: File) => void handleAttach(file) } : {})}
|