@adatechnology/conversations-ui 0.4.1 → 0.5.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/README.md +169 -0
- package/dist/{ConversationSimulatorPanel-DnXElfY8.d.ts → ConversationSimulatorPanel-CrfFjiWh.d.ts} +2 -268
- package/dist/{chunk-PJNR6UMN.js → chunk-CBYEHPX6.js} +1 -63
- package/dist/{chunk-HMCOTZAX.js → chunk-HOKNKXIT.js} +1 -1
- package/dist/{chunk-4P5KMWOI.js → chunk-JXJCPN34.js} +327 -522
- package/dist/chunk-MRVFFJEH.js +64 -0
- package/dist/chunk-RJMSM3YA.js +249 -0
- package/dist/chunk-WSFGNR5R.js +55 -0
- package/dist/flows/index.js +5 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +783 -807
- package/dist/participant/index.d.ts +162 -0
- package/dist/participant/index.js +1448 -0
- package/dist/preview/index.d.ts +4 -2
- package/dist/preview/index.js +4 -2
- package/dist/quickReply.types-B4gmZsSr.d.ts +274 -0
- package/dist/styles.css +591 -0
- package/package.json +9 -5
- package/src/ChannelIcon.tsx +2 -0
- package/src/MessageBubble.test.tsx +2 -2
- package/src/MessageText.test.tsx +59 -0
- package/src/MessageText.tsx +20 -12
- package/src/MessageTextCopiedBadge.tsx +7 -0
- package/src/StatusTicks.test.tsx +30 -0
- package/src/StatusTicks.tsx +24 -9
- package/src/buildOutput.test.ts +41 -0
- package/src/channelCapability.test.ts +17 -0
- package/src/conversationChannel.test.ts +30 -1
- package/src/conversationChannel.ts +19 -1
- package/src/participant/ParticipantAttachmentItem.tsx +77 -0
- package/src/participant/ParticipantComposer.test.tsx +97 -0
- package/src/participant/ParticipantComposer.tsx +167 -0
- package/src/participant/ParticipantConversations.test.tsx +90 -0
- package/src/participant/ParticipantConversations.tsx +42 -0
- package/src/participant/ParticipantConversationsScreen.tsx +67 -0
- package/src/participant/ParticipantInbox.test.tsx +147 -0
- package/src/participant/ParticipantInbox.tsx +168 -0
- package/src/participant/ParticipantLoadState.tsx +27 -0
- package/src/participant/ParticipantMessageBubble.test.tsx +165 -0
- package/src/participant/ParticipantMessageBubble.tsx +185 -0
- package/src/participant/ParticipantThread.test.tsx +132 -0
- package/src/participant/ParticipantThread.tsx +174 -0
- package/src/participant/ParticipantThreadScreen.tsx +135 -0
- package/src/participant/bindAttachmentUrlResolver.test.ts +25 -0
- package/src/participant/bindAttachmentUrlResolver.ts +7 -0
- package/src/participant/index.ts +25 -0
- package/src/participant/participant.types.ts +43 -0
- package/src/participant/participantApi.types.ts +59 -0
- package/src/participant/participantAttachmentUrl.test.ts +32 -0
- package/src/participant/participantAttachmentUrl.ts +10 -0
- package/src/participant/participantBubbleActions.test.ts +44 -0
- package/src/participant/participantBubbleActions.ts +40 -0
- package/src/participant/participantDrafts.test.ts +65 -0
- package/src/participant/participantDrafts.ts +50 -0
- package/src/participant/participantFileValidation.test.ts +86 -0
- package/src/participant/participantFileValidation.ts +72 -0
- package/src/participant/participantFixtures.test-helper.ts +41 -0
- package/src/participant/participantGrouping.test.ts +138 -0
- package/src/participant/participantGrouping.ts +77 -0
- package/src/participant/participantInboxState.test.ts +69 -0
- package/src/participant/participantInboxState.ts +74 -0
- package/src/participant/participantLabels.ts +86 -0
- package/src/participant/participantLoadView.test.ts +23 -0
- package/src/participant/participantLoadView.ts +21 -0
- package/src/participant/participantMessages.test.ts +128 -0
- package/src/participant/participantMessages.ts +73 -0
- package/src/participant/participantNewMessages.test.ts +54 -0
- package/src/participant/participantNewMessages.ts +29 -0
- package/src/participant/participantQuickReply.test.ts +23 -0
- package/src/participant/participantQuickReply.ts +6 -0
- package/src/participant/participantScroll.test.ts +69 -0
- package/src/participant/participantScroll.ts +43 -0
- package/src/participant/participantSendController.test.ts +201 -0
- package/src/participant/participantSendController.ts +134 -0
- package/src/participant/participantSendStates.ts +31 -0
- package/src/participant/participantTheme.ts +20 -0
- package/src/participant/participantThreadState.test.ts +75 -0
- package/src/participant/participantThreadState.ts +59 -0
- package/src/participant/shouldMarkRead.test.ts +24 -0
- package/src/participant/shouldMarkRead.ts +16 -0
- package/src/participant/useDocumentVisibility.ts +18 -0
- package/src/participant/useParticipantConversationView.ts +60 -0
- package/src/participant/useParticipantDraft.ts +45 -0
- package/src/participant/useParticipantInbox.ts +57 -0
- package/src/participant/useParticipantMarkRead.ts +26 -0
- package/src/participant/useParticipantRevalidation.ts +36 -0
- package/src/participant/useParticipantSend.ts +84 -0
- package/src/participant/useParticipantThread.ts +116 -0
- package/src/participant/useParticipantThreadScroll.ts +26 -0
- package/src/participant/useStickToBottom.ts +77 -0
- package/src/preview/ConversationSimulatorPanel.tsx +8 -0
- package/src/styles.css +160 -0
- package/src/types.ts +3 -1
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# @adatechnology/conversations-ui
|
|
2
|
+
|
|
3
|
+
Telas de conversa parametrizáveis por endpoint, tema e capacidades. Subpaths: `.` (inbox/mensagens),
|
|
4
|
+
`/flows`, `/whatsapp`, `/preview`, `/participant` e `/styles.css`.
|
|
5
|
+
|
|
6
|
+
## Visão do participante (`/participant`)
|
|
7
|
+
|
|
8
|
+
Tela de conversa do lado de **quem conversa com a empresa** (app do motorista, portal do cliente). Não usa
|
|
9
|
+
`ConversationsProvider`, não puxa `@xyflow/react` nem o `ConversationsWorkspace`, e não depende de Tailwind:
|
|
10
|
+
todas as classes são `.cv-p-*` e vêm de `@adatechnology/conversations-ui/styles.css`.
|
|
11
|
+
|
|
12
|
+
`@adatechnology/conversations-ui/styles.css` é **requisito**: sem ele a tela fica sem layout e o `MessageText` e o
|
|
13
|
+
`StatusTicks` perdem a formatação (negrito, itálico, tachado, tiques coloridos). Importe-o uma vez na raiz do app.
|
|
14
|
+
|
|
15
|
+
`MessageText` **copia ao tocar** por padrão (como na 0.3.1); `copyOnClick={false}` desliga. O aviso de cópia usa
|
|
16
|
+
`copiedLabel` (padrão `Copied`; passe `copiedLabel="Copiado!"` para manter o português).
|
|
17
|
+
|
|
18
|
+
### Uso mínimo
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { ParticipantConversations } from '@adatechnology/conversations-ui/participant'
|
|
22
|
+
import '@adatechnology/conversations-ui/styles.css'
|
|
23
|
+
|
|
24
|
+
const SUBJECT_GROUPS = [
|
|
25
|
+
{ subjectType: 'invoice', label: 'Faturas' },
|
|
26
|
+
{ subjectType: 'trip', label: 'Viagens' },
|
|
27
|
+
] as const
|
|
28
|
+
|
|
29
|
+
function ConversationsRoute() {
|
|
30
|
+
const { subjectType, subjectId } = useParams()
|
|
31
|
+
const navigate = useNavigate()
|
|
32
|
+
const selected = subjectType && subjectId ? { subjectType, subjectId } : undefined
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<ParticipantConversations
|
|
36
|
+
api={participantApi}
|
|
37
|
+
subjectGroups={SUBJECT_GROUPS}
|
|
38
|
+
selected={selected}
|
|
39
|
+
onSelect={(subject) => navigate(`/conversas/${subject.subjectType}/${subject.subjectId}`)}
|
|
40
|
+
onBack={() => navigate('/conversas')}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Contrato de roteamento
|
|
47
|
+
|
|
48
|
+
O componente é **controlado pela rota**: `selected === undefined` mostra a lista, definido mostra a conversa.
|
|
49
|
+
**Monte-o UMA vez acima das duas rotas** (`/conversas` e `/conversas/:subjectType/:subjectId`), com o mesmo
|
|
50
|
+
elemento. O cache da lista e os rascunhos por conversa (texto e anexos) vivem dentro dele; se cada rota
|
|
51
|
+
montasse a sua instância, voltar da conversa para a lista recarregaria tudo e perderia o rascunho.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<Route path="/conversas" element={<ConversationsRoute />} />
|
|
55
|
+
<Route path="/conversas/:subjectType/:subjectId" element={<ConversationsRoute />} />
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Capacidades opcionais existem **por ausência de prop**: sem `onBack` não há botão de voltar, sem
|
|
59
|
+
`renderSubjectCard` não há cartão do assunto, sem `onOpenSubject` o título não é clicável, sem
|
|
60
|
+
`quickReplies` não há faixa de respostas rápidas, sem `pendingMessages` não há fila do host. Sem
|
|
61
|
+
`api.subscribe`, a tela revalida ao montar e em `focus`, `visibilitychange` e `online`; com `subscribe`, revalida também em `online` (um evento pode ter se perdido) e marca como lido ao voltar a ficar visível. Sem
|
|
62
|
+
`api.openConversation`, uma conversa fora da primeira página da lista aparece como "não encontrada"
|
|
63
|
+
(`labels.notFound`).
|
|
64
|
+
|
|
65
|
+
### Adapter REST de `ParticipantConversationsApi`
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { z } from 'zod'
|
|
69
|
+
|
|
70
|
+
import { participantConversationPageSchema, participantMessageSchema } from '@adatechnology/conversation-contracts'
|
|
71
|
+
import type { ParticipantConversationsApi } from '@adatechnology/conversations-ui/participant'
|
|
72
|
+
|
|
73
|
+
async function request(path: string, init?: RequestInit): Promise<unknown> {
|
|
74
|
+
const response = await fetch(`/api/v1${path}`, { ...init, credentials: 'include' })
|
|
75
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
76
|
+
return ((await response.json()) as { data: unknown }).data
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function conversationPath({ subjectType, subjectId }: { subjectType: string; subjectId: string }): string {
|
|
80
|
+
return `/conversations/${encodeURIComponent(subjectType)}/${encodeURIComponent(subjectId)}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const participantApi: ParticipantConversationsApi = {
|
|
84
|
+
listConversations: async ({ cursor } = {}) =>
|
|
85
|
+
participantConversationPageSchema.parse(
|
|
86
|
+
await request(`/conversations${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ''}`),
|
|
87
|
+
),
|
|
88
|
+
fetchMessages: async (subject, { before, limit } = {}) =>
|
|
89
|
+
z.array(participantMessageSchema).parse(
|
|
90
|
+
await request(
|
|
91
|
+
`${conversationPath(subject)}/messages?limit=${limit ?? 30}${before ? `&before=${encodeURIComponent(before)}` : ''}`,
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
sendMessage: async ({ subject, clientMessageId, text }) => {
|
|
95
|
+
const message = participantMessageSchema.parse(
|
|
96
|
+
await request(`${conversationPath(subject)}/messages`, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': clientMessageId },
|
|
99
|
+
body: JSON.stringify({ text }),
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
return { outcome: 'sent', message }
|
|
103
|
+
},
|
|
104
|
+
markRead: async (subject) => {
|
|
105
|
+
await request(`${conversationPath(subject)}/read`, { method: 'POST' })
|
|
106
|
+
},
|
|
107
|
+
resolveAttachmentUrl: async (attachment) =>
|
|
108
|
+
z.object({ url: z.string() }).parse(await request(`/attachments/${encodeURIComponent(attachment.id)}/url`)).url,
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
A resposta do servidor é entrada não confiável: valide com os schemas do `@adatechnology/conversation-contracts`
|
|
113
|
+
(`participantConversationPageSchema`, `participantMessageSchema`) e use `encodeURIComponent` em todo segmento de URL
|
|
114
|
+
que vem de `subjectType`, `subjectId`, cursor ou id.
|
|
115
|
+
|
|
116
|
+
**Eco de `clientMessageId` (obrigatório).** Toda mensagem criada por `sendMessage` volta com o `clientMessageId`
|
|
117
|
+
recebido (o mesmo valor do `Idempotency-Key`): é por ele que a bolha local é substituída pela mensagem do servidor.
|
|
118
|
+
Sem o eco, a bolha local fica duplicada ao lado da mensagem real. O servidor também precisa tratar com segurança
|
|
119
|
+
duas requisições simultâneas com a mesma chave (toque duplo, reenvio): uma cria, a outra devolve a mesma mensagem.
|
|
120
|
+
|
|
121
|
+
Ao tocar em enviar, o campo é limpo na hora e a **bolha passa a ser a dona** do texto e dos arquivos (o segundo
|
|
122
|
+
toque não envia nada). `sendMessage` que lança significa falha: a bolha mostra "reenviar" (repete o **mesmo**
|
|
123
|
+
`clientMessageId`; use-o como `Idempotency-Key`), "editar" (devolve texto e arquivos ao campo e remove a bolha) e
|
|
124
|
+
"descartar". Uma bolha que falhou sobrevive a sair e voltar da conversa. Para guardar na fila offline do host,
|
|
125
|
+
devolva `{ outcome: 'queued' }` e devolva a fila durável em `pendingMessages`: a bolha fica como "na fila" até o host
|
|
126
|
+
ou o servidor refletirem o mesmo `clientMessageId`. Falha de uma mensagem que veio do servidor mostra só "Falhou";
|
|
127
|
+
falha de uma pendente do host só ganha "reenviar" se `onRetryPending` for passado. O pacote não conhece IndexedDB.
|
|
128
|
+
|
|
129
|
+
Limites do host: devolver `queued` sem refletir a mensagem em `pendingMessages` deixa uma bolha "na fila" sem
|
|
130
|
+
nenhuma ação até a conversa desmontar (ela segura os `File`); `sendMessage` precisa terminar com timeout (uma
|
|
131
|
+
promise que nunca resolve trava o botão de enviar daquele assunto); e se o host tira a mensagem da fila antes de o
|
|
132
|
+
servidor refletir, deve emitir `conversation-changed` para a conversa recarregar.
|
|
133
|
+
|
|
134
|
+
`api` **precisa ser estável** (crie o adapter uma vez, fora do render, ou memorize-o): uma instância nova a cada
|
|
135
|
+
render refaz a carga da lista e reinscreve os eventos de `subscribe` a cada renderização. Métodos de adapter escritos como classe funcionam, o pacote preserva o `this`.
|
|
136
|
+
|
|
137
|
+
### Aviso: `direction` é sempre na perspectiva da empresa
|
|
138
|
+
|
|
139
|
+
`ParticipantMessage.direction` descreve a mensagem **do ponto de vista da empresa**: `inbound` chegou à
|
|
140
|
+
empresa, ou seja, foi escrita pelo participante (é a bolha "minha"); `outbound` saiu da empresa. O adapter do
|
|
141
|
+
app **não inverte nada**.
|
|
142
|
+
|
|
143
|
+
### Variáveis `--cv-p-*`
|
|
144
|
+
|
|
145
|
+
Defina no `.cv-p`, no wrapper (`className`) ou em qualquer ancestral. Há também a prop `theme`
|
|
146
|
+
(`primaryColor` → `--cv-p-accent`, `backgroundColor` → `--cv-p-surface`, `textPrimary` → `--cv-p-text`,
|
|
147
|
+
`textSecondary` → `--cv-p-text-muted`).
|
|
148
|
+
|
|
149
|
+
| Variável | Para que serve | Padrão (claro) |
|
|
150
|
+
| --- | --- | --- |
|
|
151
|
+
| `--cv-p-surface` | fundo da tela | `#ffffff` |
|
|
152
|
+
| `--cv-p-surface-raised` | fundo de bolhas recebidas e campos | `#f3f4f6` |
|
|
153
|
+
| `--cv-p-text` | texto principal | `#111827` |
|
|
154
|
+
| `--cv-p-text-muted` | texto secundário | `#5b6573` |
|
|
155
|
+
| `--cv-p-border` | bordas | `#d5d9df` |
|
|
156
|
+
| `--cv-p-accent` | destaque, botão primário, bolha própria | `#a85a1c` |
|
|
157
|
+
| `--cv-p-accent-contrast` | texto sobre o destaque | `#ffffff` |
|
|
158
|
+
| `--cv-p-highlight` | fundo de "espera sua resposta" | `#fbefe2` |
|
|
159
|
+
| `--cv-p-danger` | erro e falha de envio | `#c62828` |
|
|
160
|
+
| `--cv-p-radius` | raio dos cantos | `0` |
|
|
161
|
+
|
|
162
|
+
O tema escuro segue a classe `.dark` (mesma do restante do pacote). Os nomes `.cv-p-*` e `--cv-p-*` são API
|
|
163
|
+
pública e não mudam sem nova versão maior.
|
|
164
|
+
|
|
165
|
+
### Aviso: `ConversationChannel` foi ampliado
|
|
166
|
+
|
|
167
|
+
`ConversationChannel` agora inclui `'app'` e `'portal'`. Quem tem um `switch` exaustivo sobre o tipo (sem
|
|
168
|
+
`default`) passa a ter erro de compilação até tratar os dois novos valores. Quem só escreve o tipo não é
|
|
169
|
+
afetado.
|
package/dist/{ConversationSimulatorPanel-DnXElfY8.d.ts → ConversationSimulatorPanel-CrfFjiWh.d.ts}
RENAMED
|
@@ -1,203 +1,8 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
+
import { M as MessagePayload, b as MessageTranscription, C as ConversationChannel, Q as QuickReply, c as QuickReplyInput, d as QuickReplyAttachment, S as StoredAttachmentSendResult, I as InteractiveSelection } from './quickReply.types-B4gmZsSr.js';
|
|
3
4
|
import { InteractiveReplyOption, InboundMediaType } from '@adatechnology/meta-whatsapp-contracts/testing';
|
|
4
5
|
|
|
5
|
-
interface ConversationsUIConfig {
|
|
6
|
-
apiBaseUrl: string;
|
|
7
|
-
theme?: ConversationsTheme;
|
|
8
|
-
features?: ConversationsFeatures;
|
|
9
|
-
}
|
|
10
|
-
interface ConversationsTheme {
|
|
11
|
-
primaryColor?: string;
|
|
12
|
-
backgroundColor?: string;
|
|
13
|
-
bubbleSent?: string;
|
|
14
|
-
bubbleReceived?: string;
|
|
15
|
-
textPrimary?: string;
|
|
16
|
-
textSecondary?: string;
|
|
17
|
-
}
|
|
18
|
-
interface ConversationsFeatures {
|
|
19
|
-
audio?: boolean;
|
|
20
|
-
documents?: boolean;
|
|
21
|
-
emoji?: boolean;
|
|
22
|
-
darkMode?: boolean;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Recorte do bloco `interactive` da Meta que a UI precisa para desenhar o menu. Fica solto (e não
|
|
26
|
-
* espelhando o contrato inteiro) porque o que chega do banco é o payload cru já enviado ao
|
|
27
|
-
* WhatsApp: qualquer campo que a UI não conheça é ignorado, nunca causa erro de render.
|
|
28
|
-
*/
|
|
29
|
-
interface InteractiveOption {
|
|
30
|
-
id: string;
|
|
31
|
-
title: string;
|
|
32
|
-
description?: string;
|
|
33
|
-
}
|
|
34
|
-
interface InteractiveSection {
|
|
35
|
-
title?: string;
|
|
36
|
-
rows?: InteractiveOption[];
|
|
37
|
-
}
|
|
38
|
-
interface InteractivePayload {
|
|
39
|
-
type?: 'button' | 'list' | string;
|
|
40
|
-
header?: {
|
|
41
|
-
text?: string;
|
|
42
|
-
};
|
|
43
|
-
body?: {
|
|
44
|
-
text?: string;
|
|
45
|
-
};
|
|
46
|
-
footer?: {
|
|
47
|
-
text?: string;
|
|
48
|
-
};
|
|
49
|
-
action?: {
|
|
50
|
-
/** Rótulo do botão que abre a lista — só existe em `type: 'list'`. */
|
|
51
|
-
button?: string;
|
|
52
|
-
sections?: InteractiveSection[];
|
|
53
|
-
buttons?: {
|
|
54
|
-
reply?: InteractiveOption;
|
|
55
|
-
}[];
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
/** Como o cliente respondeu a um menu: por botão ou por item de lista. */
|
|
59
|
-
type InteractiveSelection = {
|
|
60
|
-
readonly kind: 'button' | 'list';
|
|
61
|
-
readonly option: InteractiveOption;
|
|
62
|
-
};
|
|
63
|
-
interface MessagePayload {
|
|
64
|
-
id: string;
|
|
65
|
-
type: 'text' | 'image' | 'video' | 'audio' | 'document' | 'sticker' | 'template' | 'interactive';
|
|
66
|
-
/** Payload cru da mensagem. Em `type: 'interactive'`, carrega o menu que o cliente vê. */
|
|
67
|
-
payload?: InteractivePayload | null;
|
|
68
|
-
content?: string;
|
|
69
|
-
caption?: string;
|
|
70
|
-
mediaUrl?: string;
|
|
71
|
-
base64?: string;
|
|
72
|
-
uploadId?: string;
|
|
73
|
-
mediaId?: string;
|
|
74
|
-
mimeType?: string;
|
|
75
|
-
filename?: string;
|
|
76
|
-
sizeBytes?: number;
|
|
77
|
-
direction: 'inbound' | 'outbound';
|
|
78
|
-
sender: 'bot' | 'customer' | 'agent';
|
|
79
|
-
timestamp: string;
|
|
80
|
-
status?: 'sent' | 'delivered' | 'read' | 'failed';
|
|
81
|
-
readAt?: string;
|
|
82
|
-
agentName?: string | null;
|
|
83
|
-
templateName?: string;
|
|
84
|
-
/**
|
|
85
|
-
* Veredito de moderação vindo do backend — a UI só exibe, nunca calcula. Dicionário no browser
|
|
86
|
-
* seria peso morto e daria veredito diferente por versão de cliente.
|
|
87
|
-
*
|
|
88
|
-
* `null`/ausente = não avaliado (moderação desligada, ou mensagem anterior ao recurso), que é
|
|
89
|
-
* diferente de avaliado e limpo.
|
|
90
|
-
*/
|
|
91
|
-
moderation?: {
|
|
92
|
-
isOffensive: boolean;
|
|
93
|
-
terms: string[];
|
|
94
|
-
} | null;
|
|
95
|
-
/**
|
|
96
|
-
* Transcrição do áudio, vinda do backend — a UI só exibe, nunca transcreve. Rodar STT no browser
|
|
97
|
-
* exigiria baixar modelo por aba e daria resultado diferente por versão de cliente.
|
|
98
|
-
*
|
|
99
|
-
* `null`/ausente = não avaliado, que é diferente de `'done'` com texto vazio (áudio em silêncio,
|
|
100
|
-
* já processado). É essa distinção que decide se o balão oferece "transcrever" ou "sem fala
|
|
101
|
-
* detectada".
|
|
102
|
-
*/
|
|
103
|
-
transcription?: MessageTranscription | null;
|
|
104
|
-
isFirstInGroup?: boolean;
|
|
105
|
-
isLastInGroup?: boolean;
|
|
106
|
-
}
|
|
107
|
-
type TranscriptionStatus = 'pending' | 'done' | 'failed' | 'unsupported';
|
|
108
|
-
/**
|
|
109
|
-
* Quando transcrever, escolhido nas configurações da empresa. Espelha o
|
|
110
|
-
* `TranscriptionMode` de `@adatechnology/meta-whatsapp-contracts`; declarado aqui para o pacote de
|
|
111
|
-
* UI não obrigar quem só desenha telas a instalar os contratos do backend.
|
|
112
|
-
*/
|
|
113
|
-
type TranscriptionMode = 'auto' | 'onDemand';
|
|
114
|
-
interface MessageTranscription {
|
|
115
|
-
status: TranscriptionStatus;
|
|
116
|
-
text?: string | null;
|
|
117
|
-
/** ISO 639-1 ou nome do idioma, conforme o engine. Exibido como dica, não interpretado. */
|
|
118
|
-
language?: string | null;
|
|
119
|
-
engine?: string | null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Canal de origem da conversa e o que cada um permite.
|
|
124
|
-
*
|
|
125
|
-
* Existe porque as regras que a inbox precisa respeitar não são do WhatsApp, são **de cada canal**:
|
|
126
|
-
* a janela de sessão, o jeito de reabrir a conversa e o formato do identificador do contato mudam
|
|
127
|
-
* entre WhatsApp, Messenger, Instagram e chat de site. Tratar a regra do WhatsApp como universal
|
|
128
|
-
* faria a UI bloquear o composer num chat de site, onde janela nenhuma existe.
|
|
129
|
-
*
|
|
130
|
-
* `whatsapp` é o padrão em todo lugar: instalações que ainda não informam canal continuam
|
|
131
|
-
* funcionando exatamente como antes.
|
|
132
|
-
*/
|
|
133
|
-
declare const CONVERSATION_CHANNEL: {
|
|
134
|
-
readonly WHATSAPP: "whatsapp";
|
|
135
|
-
readonly MESSENGER: "messenger";
|
|
136
|
-
readonly INSTAGRAM: "instagram";
|
|
137
|
-
readonly WEBCHAT: "webchat";
|
|
138
|
-
readonly EMAIL: "email";
|
|
139
|
-
};
|
|
140
|
-
type ConversationChannel = (typeof CONVERSATION_CHANNEL)[keyof typeof CONVERSATION_CHANNEL];
|
|
141
|
-
declare const DEFAULT_CONVERSATION_CHANNEL: ConversationChannel;
|
|
142
|
-
/** Como o canal reabre uma conversa fora da janela de sessão. */
|
|
143
|
-
declare const REOPEN_MECHANISM: {
|
|
144
|
-
readonly TEMPLATE: "template";
|
|
145
|
-
readonly TAG: "tag";
|
|
146
|
-
readonly NONE: "none";
|
|
147
|
-
};
|
|
148
|
-
type ReopenMechanism = (typeof REOPEN_MECHANISM)[keyof typeof REOPEN_MECHANISM];
|
|
149
|
-
/** Natureza do identificador do contato — decide como exibi-lo. */
|
|
150
|
-
declare const HANDLE_KIND: {
|
|
151
|
-
readonly PHONE: "phone";
|
|
152
|
-
readonly USERNAME: "username";
|
|
153
|
-
readonly SESSION: "session";
|
|
154
|
-
readonly EMAIL: "email";
|
|
155
|
-
};
|
|
156
|
-
type HandleKind = (typeof HANDLE_KIND)[keyof typeof HANDLE_KIND];
|
|
157
|
-
/**
|
|
158
|
-
* Só o que é **exibição** do canal — rótulo, ícone, como reabrir, como mostrar o identificador.
|
|
159
|
-
* Capacidade de **comportamento** (confirma leitura, tem janela e de quantas horas, aceita anexo e
|
|
160
|
-
* áudio, precisa de transporte próprio) não mora mais aqui: para canal que
|
|
161
|
-
* `@adatechnology/conversation-contracts` conhece, ela vem de `channelCapabilityFor` (RF10). Manter
|
|
162
|
-
* os dois tipos de capacidade juntos foi o que criou a "janela" duplicada (`windowHours`) que a
|
|
163
|
-
* spec 211 mandou apagar — a mesma verdade não pode morar em dois lugares.
|
|
164
|
-
*/
|
|
165
|
-
type ChannelCapabilities = {
|
|
166
|
-
readonly label: string;
|
|
167
|
-
readonly icon: string;
|
|
168
|
-
readonly reopenMechanism: ReopenMechanism;
|
|
169
|
-
readonly handleKind: HandleKind;
|
|
170
|
-
};
|
|
171
|
-
declare const CHANNEL_CAPABILITIES: Readonly<Record<ConversationChannel, ChannelCapabilities>>;
|
|
172
|
-
declare function capabilitiesOf(channel: ConversationChannel | undefined): ChannelCapabilities;
|
|
173
|
-
declare const CHANNEL_FILTER_ALL = "all";
|
|
174
|
-
type ChannelFilter = ConversationChannel | typeof CHANNEL_FILTER_ALL;
|
|
175
|
-
type ChannelFilterOption = {
|
|
176
|
-
readonly value: ChannelFilter;
|
|
177
|
-
readonly label: string;
|
|
178
|
-
};
|
|
179
|
-
/**
|
|
180
|
-
* Opções derivadas do que existe na lista, não do catálogo inteiro: oferecer Instagram numa conta
|
|
181
|
-
* que só tem WhatsApp promete um recorte que nunca traz resultado.
|
|
182
|
-
*
|
|
183
|
-
* Devolve vazio com menos de dois canais — um filtro de opção única não filtra nada, e a barra só
|
|
184
|
-
* ocuparia espaço. O host usa isso para esconder a seção.
|
|
185
|
-
*/
|
|
186
|
-
declare function channelFiltersFor(conversations: readonly {
|
|
187
|
-
readonly channel?: ConversationChannel | undefined;
|
|
188
|
-
}[]): ChannelFilterOption[];
|
|
189
|
-
type FormatContactHandleParams = {
|
|
190
|
-
readonly handle: string;
|
|
191
|
-
readonly channel?: ConversationChannel | undefined;
|
|
192
|
-
};
|
|
193
|
-
/**
|
|
194
|
-
* Exibição do identificador conforme a natureza dele. Formatar tudo como telefone — o que a UI
|
|
195
|
-
* fazia — transforma um `@perfil` do Instagram em dígitos sem sentido.
|
|
196
|
-
*/
|
|
197
|
-
declare function formatContactHandle(params: FormatContactHandleParams): string;
|
|
198
|
-
/** Bandeira só faz sentido quando o identificador é telefone. */
|
|
199
|
-
declare function contactFlag(params: FormatContactHandleParams): string;
|
|
200
|
-
|
|
201
6
|
type ResolveMediaUrl = (message: MessagePayload) => Promise<string | null>;
|
|
202
7
|
interface MediaRendererProps {
|
|
203
8
|
message: MessagePayload;
|
|
@@ -215,77 +20,6 @@ interface MediaRendererProps {
|
|
|
215
20
|
}
|
|
216
21
|
declare function MediaRenderer({ message, onLightbox, onResolveUrl, onTranscribeAudio, className, }: MediaRendererProps): react.JSX.Element | null;
|
|
217
22
|
|
|
218
|
-
/**
|
|
219
|
-
* Mensagem pronta cadastrada pelo produto e oferecida no composer.
|
|
220
|
-
*
|
|
221
|
-
* Difere do `QuickReply` dos chips (`MessageComposer`): aquela vive em código no host; esta vem da
|
|
222
|
-
* API, tem atalho para o `/` e é editada pela tela de cadastro.
|
|
223
|
-
*/
|
|
224
|
-
type QuickReply = {
|
|
225
|
-
readonly id: string;
|
|
226
|
-
/** Até 40 caracteres — é o que o atendente lê na lista. */
|
|
227
|
-
readonly title: string;
|
|
228
|
-
/** Casa `^[a-z0-9-]{1,20}$`: é digitado depois do `/`, sem acento nem espaço. */
|
|
229
|
-
readonly shortcut: string;
|
|
230
|
-
/** Até 1000 caracteres, com `{{marcador}}` onde entra o dado da conversa. */
|
|
231
|
-
readonly body: string;
|
|
232
|
-
/** Ausente em host sem anexos: o contrato continua o mesmo para quem não os oferece. */
|
|
233
|
-
readonly attachments?: readonly QuickReplyAttachment[];
|
|
234
|
-
};
|
|
235
|
-
/** Teto de anexos por mensagem pronta — o mesmo que a API aplica, para a tela recusar antes. */
|
|
236
|
-
declare const QUICK_REPLY_ATTACHMENT_LIMIT = 10;
|
|
237
|
-
/** Arquivo já guardado pelo host; o `uploadId` é a única chave que o pacote devolve ao servidor. */
|
|
238
|
-
type QuickReplyAttachment = {
|
|
239
|
-
readonly uploadId: string;
|
|
240
|
-
readonly filename: string;
|
|
241
|
-
readonly mimeType: string;
|
|
242
|
-
readonly sizeBytes: number;
|
|
243
|
-
};
|
|
244
|
-
/**
|
|
245
|
-
* Item da fila do composer. `local` ainda não subiu (veio do clipe); `stored` já está no servidor
|
|
246
|
-
* (veio de uma mensagem pronta) e é enviado por referência, sem baixar e subir de novo.
|
|
247
|
-
*/
|
|
248
|
-
type QueuedAttachment = {
|
|
249
|
-
readonly kind: 'local';
|
|
250
|
-
readonly localId: string;
|
|
251
|
-
readonly file: File;
|
|
252
|
-
} | {
|
|
253
|
-
readonly kind: 'stored';
|
|
254
|
-
readonly uploadId: string;
|
|
255
|
-
readonly filename: string;
|
|
256
|
-
readonly mimeType: string;
|
|
257
|
-
readonly sizeBytes: number;
|
|
258
|
-
/** Carregada sob demanda só para imagem — a lista não pode abrir N URLs assinadas de uma vez. */
|
|
259
|
-
readonly previewUrl?: string;
|
|
260
|
-
};
|
|
261
|
-
/** Resultado por arquivo: o envio em lote falha parcialmente, e só o que falhou fica na fila. */
|
|
262
|
-
type StoredAttachmentSendResult = {
|
|
263
|
-
readonly uploadId: string;
|
|
264
|
-
readonly status: 'sent' | 'failed' | 'skipped';
|
|
265
|
-
readonly errorCode?: string;
|
|
266
|
-
};
|
|
267
|
-
/** O que a tela de cadastro manda para criar ou atualizar; o `id` é do servidor. */
|
|
268
|
-
type QuickReplyInput = {
|
|
269
|
-
readonly title: string;
|
|
270
|
-
readonly shortcut: string;
|
|
271
|
-
readonly body: string;
|
|
272
|
-
/** Ordem do cadastro é a ordem de envio; ausente não mexe nos anexos já gravados. */
|
|
273
|
-
readonly attachmentUploadIds?: readonly string[];
|
|
274
|
-
};
|
|
275
|
-
/**
|
|
276
|
-
* Dado da conversa que um texto pode citar. Uma lista só alimenta a prévia, a inserção e o botão de
|
|
277
|
-
* variáveis — antes eram duas props de formato diferente, e as telas divergiam por isso.
|
|
278
|
-
*/
|
|
279
|
-
type ConversationVariable = {
|
|
280
|
-
readonly id: string;
|
|
281
|
-
/** Rótulo do botão "Inserir variável" (ex.: "Nome do cliente"). */
|
|
282
|
-
readonly label: string;
|
|
283
|
-
/** Como aparece no texto: `{{nome}}`. */
|
|
284
|
-
readonly marker: string;
|
|
285
|
-
/** Vazio quando a conversa não tem o dado: a variável não é oferecida. */
|
|
286
|
-
readonly value: string;
|
|
287
|
-
};
|
|
288
|
-
|
|
289
23
|
/**
|
|
290
24
|
* Gravação de áudio no simulador, pelo microfone do próprio navegador.
|
|
291
25
|
*
|
|
@@ -917,4 +651,4 @@ type ConversationSimulatorPanelProps = Omit<ConversationPreviewProps, 'placehold
|
|
|
917
651
|
};
|
|
918
652
|
declare function ConversationSimulatorPanel({ onClose, channel, displayHandle, displayNumber, labels, headerActions, ...previewProps }: ConversationSimulatorPanelProps): react.JSX.Element;
|
|
919
653
|
|
|
920
|
-
export {
|
|
654
|
+
export { AudioRecorderButton as A, acceptsMediaKind as B, type ConversationSummary as C, DEFAULT_AUDIO_RECORDER_BUTTON_LABELS as D, assertPreviewEnvironment as E, createPreviewMediaPoster as F, createPreviewMediaUploader as G, createPreviewWebhookClient as H, isConversationSimulatorClient as I, mediaKindOf as J, mediaTypeOf as K, type ListConversationsParams as L, signPreviewPayload as M, simulatorPanelLabelsOf as N, toConversationSimulatorClient as O, type PreviewUploadedMedia$1 as P, type ListDocumentsParams as Q, type ResolveMediaUrl as R, type SSEProvider as S, type ToSimulatorClientParams as T, type ConversationTemplate as U, type CompanyDocument as V, type CompanyDocumentPage as W, type ConversationDocumentPage as X, DEFAULT_MAX_RECORDING_MILLISECONDS as Y, MediaRenderer as Z, type MediaRendererProps as _, type ConversationEventSource as a, type ConversationsApi as b, type ConversationPage as c, type ConversationDocument as d, type SendPreviewMediaParams as e, type PreviewWebhookClient as f, type AudioRecorderButtonLabels as g, type AudioRecorderButtonProps as h, ConversationPreview as i, type ConversationPreviewProps as j, type ConversationSimulatorClient as k, ConversationSimulatorPanel as l, type ConversationSimulatorPanelLabels as m, type ConversationSimulatorPanelProps as n, type CreatePreviewMediaUploaderParams as o, type CreatePreviewWebhookClientParams as p, DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS as q, DEFAULT_MEDIA_UPLOAD_PATH as r, PreviewInProductionError as s, PreviewMediaUploadRejectedError as t, type PreviewMediaUploadRequest as u, type PreviewUploadedMedia as v, PreviewWebhookRejectedError as w, SIMULATOR_FILE_MEDIA_KINDS as x, type SendSimulatorMediaParams as y, type SimulatorMediaKind as z };
|
|
@@ -217,72 +217,10 @@ function waToHTMLInline(text) {
|
|
|
217
217
|
return escapeHtml(text).replace(/\*([^*\n]+)\*/g, "<strong>$1</strong>").replace(/_([^_\n]+)_/g, "<em>$1</em>").replace(/~([^~\n]+)~/g, "<del>$1</del>").replace(/`([^`\n]+)`/g, "<code>$1</code>");
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
// src/useDarkMode.ts
|
|
221
|
-
import { useState, useEffect, useCallback } from "react";
|
|
222
|
-
var STORAGE_KEY = "conversations-ui-dark-mode";
|
|
223
|
-
function getInitialDark() {
|
|
224
|
-
if (typeof window === "undefined") return false;
|
|
225
|
-
const stored = localStorage.getItem(STORAGE_KEY);
|
|
226
|
-
if (stored !== null) {
|
|
227
|
-
return stored === "true";
|
|
228
|
-
}
|
|
229
|
-
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
230
|
-
}
|
|
231
|
-
function useIsDarkTheme() {
|
|
232
|
-
const [isDark, setIsDark] = useState(
|
|
233
|
-
() => typeof document !== "undefined" && document.documentElement.classList.contains("dark")
|
|
234
|
-
);
|
|
235
|
-
useEffect(() => {
|
|
236
|
-
const root = document.documentElement;
|
|
237
|
-
const observer = new MutationObserver(() => setIsDark(root.classList.contains("dark")));
|
|
238
|
-
observer.observe(root, { attributes: true, attributeFilter: ["class"] });
|
|
239
|
-
setIsDark(root.classList.contains("dark"));
|
|
240
|
-
return () => observer.disconnect();
|
|
241
|
-
}, []);
|
|
242
|
-
return isDark;
|
|
243
|
-
}
|
|
244
|
-
function useDarkMode() {
|
|
245
|
-
const [isDark, setIsDark] = useState(getInitialDark);
|
|
246
|
-
useEffect(() => {
|
|
247
|
-
const root = document.documentElement;
|
|
248
|
-
if (isDark) {
|
|
249
|
-
root.classList.add("dark");
|
|
250
|
-
} else {
|
|
251
|
-
root.classList.remove("dark");
|
|
252
|
-
}
|
|
253
|
-
localStorage.setItem(STORAGE_KEY, String(isDark));
|
|
254
|
-
}, [isDark]);
|
|
255
|
-
useEffect(() => {
|
|
256
|
-
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
257
|
-
const handleChange = (event) => {
|
|
258
|
-
const stored = localStorage.getItem(STORAGE_KEY);
|
|
259
|
-
if (stored === null) {
|
|
260
|
-
setIsDark(event.matches);
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
mediaQuery.addEventListener("change", handleChange);
|
|
264
|
-
return () => mediaQuery.removeEventListener("change", handleChange);
|
|
265
|
-
}, []);
|
|
266
|
-
const toggle = useCallback(() => {
|
|
267
|
-
setIsDark((prev) => !prev);
|
|
268
|
-
}, []);
|
|
269
|
-
return { isDark, toggle };
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// src/lib/cn.ts
|
|
273
|
-
import { clsx } from "clsx";
|
|
274
|
-
import { twMerge } from "tailwind-merge";
|
|
275
|
-
function cn(...inputs) {
|
|
276
|
-
return twMerge(clsx(inputs));
|
|
277
|
-
}
|
|
278
|
-
|
|
279
220
|
export {
|
|
280
|
-
cn,
|
|
281
221
|
parseWhatsAppFormatting,
|
|
282
222
|
ZERO_WIDTH_SPACE,
|
|
283
223
|
waToHTML,
|
|
284
224
|
htmlToWA,
|
|
285
|
-
waToHTMLInline
|
|
286
|
-
useIsDarkTheme,
|
|
287
|
-
useDarkMode
|
|
225
|
+
waToHTMLInline
|
|
288
226
|
};
|