@adatechnology/conversations-ui 0.1.0-rc.4 → 0.1.0-rc.41
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--5fIzXWY.d.ts +804 -0
- package/dist/chunk-BJNRLLDO.js +2708 -0
- package/dist/chunk-DKPXKQGC.js +110 -0
- package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
- package/dist/flows/index.d.ts +422 -5
- package/dist/flows/index.js +2389 -678
- package/dist/index.d.ts +1171 -42
- package/dist/index.js +3755 -843
- package/dist/preview/index.d.ts +157 -42
- package/dist/preview/index.js +772 -191
- package/dist/styles.css +893 -0
- package/package.json +9 -8
- package/src/AudioPlayer.tsx +8 -0
- package/src/AudioRecorderButton.test.tsx +30 -0
- package/src/AudioRecorderButton.tsx +248 -0
- package/src/AudioTranscription.test.tsx +115 -0
- package/src/AudioTranscription.tsx +252 -0
- package/src/Avatar.tsx +14 -3
- package/src/ConversationContextPanel.tsx +218 -44
- package/src/ConversationDocumentsPanel.tsx +347 -24
- package/src/ConversationHeader.test.tsx +66 -0
- package/src/ConversationHeader.tsx +163 -45
- package/src/ConversationListItem.tsx +19 -2
- package/src/ConversationLocalesProvider.tsx +28 -0
- package/src/ConversationRow.tsx +31 -7
- package/src/DarkModeToggle.test.tsx +76 -0
- package/src/DarkModeToggle.tsx +92 -0
- package/src/DocumentsLibrary.tsx +382 -0
- package/src/EmojiPicker.tsx +70 -55
- package/src/FileIcon.test.ts +83 -0
- package/src/FileIcon.tsx +88 -11
- package/src/InteractiveMessage.test.tsx +41 -0
- package/src/InteractiveMessage.tsx +146 -0
- package/src/Lightbox.tsx +18 -3
- package/src/MediaRenderer.tsx +92 -16
- package/src/MessageBubble.test.tsx +41 -0
- package/src/MessageBubble.tsx +75 -6
- package/src/MessageComposer.test.tsx +35 -0
- package/src/MessageComposer.tsx +155 -19
- package/src/RichMessageComposer.test.tsx +113 -0
- package/src/RichMessageComposer.tsx +551 -0
- package/src/SimpleEmojiPicker.tsx +5 -3
- package/src/StatusTicks.tsx +1 -1
- package/src/Toast.tsx +4 -0
- package/src/Tooltip.test.ts +42 -0
- package/src/Tooltip.tsx +167 -0
- package/src/Wallpaper.test.tsx +21 -0
- package/src/Wallpaper.tsx +67 -7
- package/src/WhatsAppMessageEditor.tsx +34 -7
- package/src/WindowExpiredNotice.tsx +12 -4
- package/src/audioRecorderFormat.test.ts +67 -0
- package/src/buildOutput.test.ts +79 -0
- package/src/composer.constant.ts +33 -0
- package/src/conversationTranscript.test.ts +57 -0
- package/src/conversationTranscript.ts +29 -4
- package/src/conversationWindow.ts +7 -5
- package/src/documentTypeLabel.test.ts +57 -0
- package/src/documents/DocumentsWorkspace.tsx +550 -0
- package/src/documents/index.ts +8 -0
- package/src/documents/labels.ts +92 -0
- package/src/emojiCatalog.test.ts +35 -0
- package/src/emojiCatalog.ts +189 -0
- package/src/flows/FlowConnectionEdge.tsx +104 -0
- package/src/flows/FlowGroupHeader.tsx +12 -2
- package/src/flows/FlowLegend.tsx +125 -0
- package/src/flows/FlowMapCanvas.tsx +15 -12
- package/src/flows/FlowMapNode.tsx +4 -1
- package/src/flows/FlowNodeCard.tsx +219 -34
- package/src/flows/FlowNodePanel.tsx +153 -39
- package/src/flows/FlowPalette.tsx +106 -69
- package/src/flows/FlowPortalNode.tsx +1 -1
- package/src/flows/FlowWhatsAppPreview.tsx +14 -3
- package/src/flows/FlowsWorkspace.tsx +1219 -0
- package/src/flows/flowCanvasModel.test.ts +456 -0
- package/src/flows/flowCanvasModel.ts +378 -0
- package/src/flows/flowEditorOps.test.ts +276 -0
- package/src/flows/flowEditorOps.ts +202 -0
- package/src/flows/flowGraph.ts +78 -53
- package/src/flows/index.ts +51 -2
- package/src/flows/labels.ts +180 -0
- package/src/flows/workspaceContract.test.ts +126 -0
- package/src/hooks/useContainerWidth.ts +35 -0
- package/src/hooks/useConversationActions.ts +56 -0
- package/src/hooks/useConversationDocuments.ts +11 -7
- package/src/hooks/useConversationList.ts +15 -9
- package/src/hooks/useConversationMessages.ts +2 -2
- package/src/hooks/useConversationRealtime.ts +10 -8
- package/src/hooks/useScrollToLatestMessage.ts +127 -0
- package/src/hooks/useUrlFilterState.ts +107 -0
- package/src/icon.constant.ts +12 -0
- package/src/index.ts +129 -13
- package/src/lib/cn.test.ts +29 -0
- package/src/lib/composer-formatting.test.ts +78 -0
- package/src/lib/composer-formatting.ts +145 -0
- package/src/lib/createMediaUrlResolver.ts +33 -0
- package/src/lib/paginated.test.ts +33 -0
- package/src/lib/paginated.ts +26 -0
- package/src/lib/whatsapp-formatting.test.tsx +37 -0
- package/src/lib/whatsapp-formatting.tsx +28 -3
- package/src/listing/index.tsx +202 -0
- package/src/pagination.constant.ts +10 -0
- package/src/preview/ConversationPreview.tsx +225 -17
- package/src/preview/ConversationSimulatorClient.ts +143 -0
- package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
- package/src/preview/ConversationSimulatorPanel.tsx +131 -0
- package/src/preview/MediaTypesPreview.tsx +87 -0
- package/src/preview/conversationPreviewFailures.test.ts +64 -0
- package/src/preview/createMockConversationsApi.ts +175 -15
- package/src/preview/createPreviewBridgeClient.test.ts +92 -0
- package/src/preview/createPreviewBridgeClient.ts +124 -0
- package/src/preview/createPreviewMediaUploader.ts +82 -0
- package/src/preview/createPreviewWebhookClient.test.ts +96 -0
- package/src/preview/createPreviewWebhookClient.ts +127 -4
- package/src/preview/index.ts +51 -3
- package/src/preview/mediaTypeOf.test.ts +15 -0
- package/src/preview/mockDocumentsSearch.test.ts +57 -0
- package/src/preview/preview.test.ts +5 -3
- package/src/preview/previewFileSamples.test.ts +151 -0
- package/src/preview/previewFileSamples.ts +74 -0
- package/src/preview/previewFixtures.ts +288 -1
- package/src/preview/previewMediaSource.test.ts +62 -0
- package/src/preview/previewMediaSource.ts +91 -0
- package/src/preview/previewMediaUploader.test.ts +61 -0
- package/src/providers/ConversationsProvider.tsx +8 -6
- package/src/providers/types.ts +185 -10
- package/src/quickReply.test.ts +58 -0
- package/src/settings/MessagesWorkspace.tsx +571 -0
- package/src/settings/TopicsForm.tsx +2 -0
- package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
- package/src/settings/TranscriptionSettingsForm.tsx +190 -0
- package/src/settings/WelcomeFarewellForm.tsx +1 -0
- package/src/settings/WhatsAppCreateTemplateForm.tsx +4 -1
- package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
- package/src/settings/WhatsAppTemplatesSettings.test.tsx +61 -0
- package/src/settings/WhatsAppTemplatesSettings.tsx +22 -2
- package/src/styles.css +858 -0
- package/src/types.ts +64 -1
- package/src/useWaitingNotifications.ts +74 -29
- package/src/workspace/BulkTemplateModal.tsx +132 -0
- package/src/workspace/ConversationPane.tsx +432 -0
- package/src/workspace/ConversationsInboxList.tsx +194 -0
- package/src/workspace/ConversationsWorkspace.tsx +423 -0
- package/src/workspace/index.ts +17 -0
- package/src/workspace/labels.test.ts +17 -0
- package/src/workspace/labels.ts +85 -0
- package/src/workspace/useConversationsInbox.ts +332 -0
- package/dist/chunk-4R6Y43DQ.js +0 -726
- package/dist/chunk-NV2RZ5KT.js +0 -56
- package/dist/types-C0PtaO7S.d.ts +0 -207
package/src/flows/labels.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { FlowConditionOperator, FlowNodeType, FlowQuestionType } from './fl
|
|
|
3
3
|
export interface FlowEditorLabels {
|
|
4
4
|
legend: Record<FlowNodeType, string>
|
|
5
5
|
startNodeTooltip: string
|
|
6
|
+
detachedNodeTooltip: string
|
|
6
7
|
liveCountTooltip: (count: number) => string
|
|
7
8
|
edgeFallbackLabel: string
|
|
8
9
|
// Rótulo por `actionKind` — o host estende esse mapa para registrar seus próprios kinds
|
|
@@ -13,10 +14,15 @@ export interface FlowEditorLabels {
|
|
|
13
14
|
nodePanel: {
|
|
14
15
|
title: string
|
|
15
16
|
contextKey: string
|
|
17
|
+
nodeName: string
|
|
18
|
+
nodeNamePlaceholder: string
|
|
19
|
+
nodeNameHint: string
|
|
16
20
|
questionType: string
|
|
17
21
|
question: string
|
|
18
22
|
options: string
|
|
19
23
|
addOption: string
|
|
24
|
+
removeOption: string
|
|
25
|
+
close: string
|
|
20
26
|
optionId: string
|
|
21
27
|
optionLabel: string
|
|
22
28
|
next: string
|
|
@@ -47,6 +53,27 @@ export interface FlowEditorLabels {
|
|
|
47
53
|
conditionTrue: string
|
|
48
54
|
conditionFalse: string
|
|
49
55
|
conditionVariableMissing: string
|
|
56
|
+
media: string
|
|
57
|
+
mediaUnavailable: string
|
|
58
|
+
}
|
|
59
|
+
quickAdd: {
|
|
60
|
+
fromHandle: string
|
|
61
|
+
title: string
|
|
62
|
+
disconnect: string
|
|
63
|
+
}
|
|
64
|
+
/** Legenda do canvas: o que cada traço e cada contorno querem dizer. */
|
|
65
|
+
legendPanel: {
|
|
66
|
+
title: string
|
|
67
|
+
nodes: string
|
|
68
|
+
connections: string
|
|
69
|
+
linear: string
|
|
70
|
+
branch: string
|
|
71
|
+
fallback: string
|
|
72
|
+
crossFlow: string
|
|
73
|
+
live: string
|
|
74
|
+
selfLoop: string
|
|
75
|
+
detached: string
|
|
76
|
+
startNode: string
|
|
50
77
|
}
|
|
51
78
|
palette: {
|
|
52
79
|
title: string
|
|
@@ -59,6 +86,8 @@ export interface FlowEditorLabels {
|
|
|
59
86
|
flowMap: {
|
|
60
87
|
nodeCount: (count: number) => string
|
|
61
88
|
openFlow: string
|
|
89
|
+
toggleToMap: string
|
|
90
|
+
toggleToDetail: string
|
|
62
91
|
}
|
|
63
92
|
flowGroup: {
|
|
64
93
|
focus: string
|
|
@@ -68,6 +97,67 @@ export interface FlowEditorLabels {
|
|
|
68
97
|
tooltip: string
|
|
69
98
|
goesTo: (label: string) => string
|
|
70
99
|
}
|
|
100
|
+
collectionChain: {
|
|
101
|
+
feeds: (label: string) => string
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Texto dos problemas encontrados por `validateGraph`. Vive aqui, e não no host, porque a tela
|
|
105
|
+
* composta é quem valida — deixar de fora obrigaria todo produto a repassar o mesmo mapa de
|
|
106
|
+
* funções só para a barra de erros aparecer.
|
|
107
|
+
*/
|
|
108
|
+
validation: FlowValidationLabels
|
|
109
|
+
/** Barra de cima, estados de carregamento e ações do editor inteiro. */
|
|
110
|
+
workspace: {
|
|
111
|
+
title: string
|
|
112
|
+
subtitle: string
|
|
113
|
+
loading: string
|
|
114
|
+
loadError: string
|
|
115
|
+
saveGraph: string
|
|
116
|
+
saving: string
|
|
117
|
+
saveSuccess: string
|
|
118
|
+
saveError: string
|
|
119
|
+
organize: string
|
|
120
|
+
organizeTooltip: string
|
|
121
|
+
discardChanges: string
|
|
122
|
+
discardTooltip: string
|
|
123
|
+
discardConfirm: string
|
|
124
|
+
unsavedChangesConfirm: string
|
|
125
|
+
}
|
|
126
|
+
flowManager: {
|
|
127
|
+
newFlow: string
|
|
128
|
+
createTitle: string
|
|
129
|
+
key: string
|
|
130
|
+
keyHint: string
|
|
131
|
+
keyInvalid: string
|
|
132
|
+
label: string
|
|
133
|
+
showInMenu: string
|
|
134
|
+
menuOptionLabel: string
|
|
135
|
+
create: string
|
|
136
|
+
creating: string
|
|
137
|
+
deleteFlow: string
|
|
138
|
+
deleteConfirm: (label: string) => string
|
|
139
|
+
createError: string
|
|
140
|
+
deleteError: string
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface FlowValidationLabels {
|
|
145
|
+
title: string
|
|
146
|
+
errors: (count: number) => string
|
|
147
|
+
warnings: (count: number) => string
|
|
148
|
+
noStart: string
|
|
149
|
+
brokenRef: (from: string, to: string) => string
|
|
150
|
+
choiceWithoutOptions: (id: string) => string
|
|
151
|
+
duplicatedOptionId: (id: string, optionId: string) => string
|
|
152
|
+
optionWithoutTarget: (id: string, optionLabel: string) => string
|
|
153
|
+
tooManyOptions: (id: string, count: number) => string
|
|
154
|
+
buttonTitleTooLong: (id: string, label: string) => string
|
|
155
|
+
listTitleTooLong: (id: string, label: string) => string
|
|
156
|
+
bodyTooLong: (id: string) => string
|
|
157
|
+
unreachable: (id: string) => string
|
|
158
|
+
deadEndQuestion: (id: string) => string
|
|
159
|
+
conditionIncomplete: (id: string) => string
|
|
160
|
+
conditionBranchMissing: (id: string, branch: string) => string
|
|
71
161
|
}
|
|
72
162
|
|
|
73
163
|
// Paridade de texto com financiamento-imobiliario-bot/apps/web/src/locales/modules/flows.ts —
|
|
@@ -81,12 +171,14 @@ export const DEFAULT_FLOW_EDITOR_LABELS: FlowEditorLabels = {
|
|
|
81
171
|
condition: 'Condição',
|
|
82
172
|
},
|
|
83
173
|
startNodeTooltip: 'Início do fluxo',
|
|
174
|
+
detachedNodeTooltip: 'Sem ligação de entrada — o bot não chega neste nó. Puxe um fio de outro card até ele.',
|
|
84
175
|
liveCountTooltip: (count) => `${count} conversa(s) ativa(s) aqui agora`,
|
|
85
176
|
edgeFallbackLabel: 'outro',
|
|
86
177
|
actionKindLabels: {
|
|
87
178
|
handoff: 'Encaminhar para atendimento',
|
|
88
179
|
rate_limited_handoff: 'Encaminhar (limite de simulações atingido)',
|
|
89
180
|
send_product_list: 'Enviar catálogo de produtos',
|
|
181
|
+
send_media: 'Enviar arquivos da biblioteca',
|
|
90
182
|
},
|
|
91
183
|
conditionOperatorLabels: {
|
|
92
184
|
'>': 'maior que',
|
|
@@ -108,10 +200,15 @@ export const DEFAULT_FLOW_EDITOR_LABELS: FlowEditorLabels = {
|
|
|
108
200
|
nodePanel: {
|
|
109
201
|
title: 'Editar nó',
|
|
110
202
|
contextKey: 'Chave (contexto)',
|
|
203
|
+
nodeName: 'Nome do nó (opcional)',
|
|
204
|
+
nodeNamePlaceholder: 'Ex.: Enviar tabela de preços',
|
|
205
|
+
nodeNameHint: 'Só aparece no editor — o cliente não vê.',
|
|
111
206
|
questionType: 'Tipo de resposta',
|
|
112
207
|
question: 'Texto da pergunta',
|
|
113
208
|
options: 'Opções (choice)',
|
|
114
209
|
addOption: 'Adicionar opção',
|
|
210
|
+
removeOption: 'Remover opção',
|
|
211
|
+
close: 'Fechar painel',
|
|
115
212
|
optionId: 'Valor',
|
|
116
213
|
optionLabel: 'Texto exibido',
|
|
117
214
|
next: 'Próximo nó',
|
|
@@ -143,6 +240,26 @@ export const DEFAULT_FLOW_EDITOR_LABELS: FlowEditorLabels = {
|
|
|
143
240
|
conditionTrue: 'Se verdadeiro →',
|
|
144
241
|
conditionFalse: 'Se falso →',
|
|
145
242
|
conditionVariableMissing: 'Se a variável ainda não foi coletada →',
|
|
243
|
+
media: 'Arquivos enviados neste ponto',
|
|
244
|
+
mediaUnavailable: 'A biblioteca de arquivos não está disponível neste painel.',
|
|
245
|
+
},
|
|
246
|
+
quickAdd: {
|
|
247
|
+
fromHandle: 'Criar o próximo nó já ligado aqui',
|
|
248
|
+
title: 'Ligar em um nó novo',
|
|
249
|
+
disconnect: 'Desligar este fio (o nó continua no fluxo)',
|
|
250
|
+
},
|
|
251
|
+
legendPanel: {
|
|
252
|
+
title: 'Legenda',
|
|
253
|
+
nodes: 'Cards',
|
|
254
|
+
connections: 'Ligações',
|
|
255
|
+
linear: 'Segue direto para o próximo',
|
|
256
|
+
branch: 'Caminho de uma opção escolhida',
|
|
257
|
+
fallback: 'Quando a resposta não casa com nenhuma opção',
|
|
258
|
+
crossFlow: 'Salta para outro fluxo',
|
|
259
|
+
live: 'Tem conversa passando por aqui agora',
|
|
260
|
+
selfLoop: 'Volta ao mesmo card — repete a pergunta',
|
|
261
|
+
detached: 'Ninguém aponta para este card: o bot não chega nele',
|
|
262
|
+
startNode: 'Onde o fluxo começa',
|
|
146
263
|
},
|
|
147
264
|
palette: {
|
|
148
265
|
title: 'Adicionar ao fluxo',
|
|
@@ -155,6 +272,8 @@ export const DEFAULT_FLOW_EDITOR_LABELS: FlowEditorLabels = {
|
|
|
155
272
|
flowMap: {
|
|
156
273
|
nodeCount: (count) => `${count} nó(s)`,
|
|
157
274
|
openFlow: 'Abrir fluxo',
|
|
275
|
+
toggleToMap: 'Mapa de fluxos',
|
|
276
|
+
toggleToDetail: 'Voltar ao editor',
|
|
158
277
|
},
|
|
159
278
|
flowGroup: {
|
|
160
279
|
focus: 'Focar neste fluxo',
|
|
@@ -164,6 +283,61 @@ export const DEFAULT_FLOW_EDITOR_LABELS: FlowEditorLabels = {
|
|
|
164
283
|
tooltip: 'Clique para abrir esse fluxo aqui do lado, ligado ao ponto de onde ele é chamado',
|
|
165
284
|
goesTo: (label) => `↪ Vai para: ${label}`,
|
|
166
285
|
},
|
|
286
|
+
collectionChain: {
|
|
287
|
+
feeds: (label) => `Alimenta: ${label}`,
|
|
288
|
+
},
|
|
289
|
+
validation: {
|
|
290
|
+
title: 'Antes de publicar',
|
|
291
|
+
errors: (count) => `${count} erro(s) — corrija antes de salvar`,
|
|
292
|
+
warnings: (count) => `${count} aviso(s)`,
|
|
293
|
+
noStart: 'O fluxo precisa de um nó inicial válido.',
|
|
294
|
+
brokenRef: (from, to) => `Nó "${from}": ligação aponta para "${to}", que não existe.`,
|
|
295
|
+
choiceWithoutOptions: (id) => `Nó "${id}": escolha sem nenhuma opção.`,
|
|
296
|
+
duplicatedOptionId: (id, optionId) => `Nó "${id}": valor de opção "${optionId}" duplicado.`,
|
|
297
|
+
optionWithoutTarget: (id, label) => `Nó "${id}": opção "${label}" não tem destino definido.`,
|
|
298
|
+
tooManyOptions: (id, count) => `Nó "${id}": ${count} opções — o WhatsApp aceita no máximo 10 em lista.`,
|
|
299
|
+
buttonTitleTooLong: (id, label) => `Nó "${id}": botão "${label}" passa de 20 caracteres.`,
|
|
300
|
+
listTitleTooLong: (id, label) => `Nó "${id}": item de lista "${label}" passa de 24 caracteres.`,
|
|
301
|
+
bodyTooLong: (id) => `Nó "${id}": texto passa de 1024 caracteres.`,
|
|
302
|
+
unreachable: (id) => `Nó "${id}" é inalcançável a partir do início do fluxo.`,
|
|
303
|
+
deadEndQuestion: (id) => `Nó "${id}": pergunta sem próximo passo definido.`,
|
|
304
|
+
conditionIncomplete: (id) => `Nó "${id}": condição incompleta — defina variável, operador e valor.`,
|
|
305
|
+
conditionBranchMissing: (id, branch) =>
|
|
306
|
+
`Nó "${id}": ramo "${branch === 'true' ? 'Verdadeiro' : 'Falso'}" sem destino definido.`,
|
|
307
|
+
},
|
|
308
|
+
workspace: {
|
|
309
|
+
title: 'Fluxos do Bot',
|
|
310
|
+
subtitle: 'Blueprint visual dos fluxos de conversa, sincronizado com o que está em produção.',
|
|
311
|
+
loading: 'Carregando fluxos…',
|
|
312
|
+
loadError: 'Não foi possível carregar os fluxos.',
|
|
313
|
+
saveGraph: 'Publicar alterações',
|
|
314
|
+
saving: 'Publicando…',
|
|
315
|
+
saveSuccess: 'Fluxo publicado! O bot já está usando a versão nova.',
|
|
316
|
+
saveError: 'Não foi possível salvar — verifique se todos os destinos apontam para nós existentes.',
|
|
317
|
+
organize: 'Organizar',
|
|
318
|
+
organizeTooltip: 'Reorganiza os nós automaticamente e salva as novas posições',
|
|
319
|
+
discardChanges: 'Desfazer alterações',
|
|
320
|
+
discardTooltip: 'Devolve os fluxos abertos ao que está publicado, descartando o que não foi salvo',
|
|
321
|
+
discardConfirm: 'Descartar todas as alterações não publicadas e voltar ao fluxo que está no ar?',
|
|
322
|
+
unsavedChangesConfirm:
|
|
323
|
+
'Você tem alterações não publicadas neste fluxo. Trocar de fluxo agora descarta essas edições. Continuar?',
|
|
324
|
+
},
|
|
325
|
+
flowManager: {
|
|
326
|
+
newFlow: 'Novo fluxo',
|
|
327
|
+
createTitle: 'Criar novo fluxo',
|
|
328
|
+
key: 'Identificador único',
|
|
329
|
+
keyHint: 'letras minúsculas, números e _ (ex.: promocoes_semana)',
|
|
330
|
+
keyInvalid: 'Use apenas letras minúsculas, números e _ (2 a 40 caracteres)',
|
|
331
|
+
label: 'Nome exibido',
|
|
332
|
+
showInMenu: 'Exibir como opção no menu principal do bot',
|
|
333
|
+
menuOptionLabel: 'Texto da opção no menu',
|
|
334
|
+
create: 'Criar fluxo',
|
|
335
|
+
creating: 'Criando…',
|
|
336
|
+
deleteFlow: 'Excluir fluxo',
|
|
337
|
+
deleteConfirm: (label) => `Excluir o fluxo "${label}"? Esta ação não pode ser desfeita.`,
|
|
338
|
+
createError: 'Não foi possível criar o fluxo.',
|
|
339
|
+
deleteError: 'Não foi possível excluir o fluxo.',
|
|
340
|
+
},
|
|
167
341
|
}
|
|
168
342
|
|
|
169
343
|
export function mergeFlowEditorLabels(override?: Partial<FlowEditorLabels>): FlowEditorLabels {
|
|
@@ -179,9 +353,15 @@ export function mergeFlowEditorLabels(override?: Partial<FlowEditorLabels>): Flo
|
|
|
179
353
|
},
|
|
180
354
|
questionTypeLabels: { ...DEFAULT_FLOW_EDITOR_LABELS.questionTypeLabels, ...override.questionTypeLabels },
|
|
181
355
|
nodePanel: { ...DEFAULT_FLOW_EDITOR_LABELS.nodePanel, ...override.nodePanel },
|
|
356
|
+
quickAdd: { ...DEFAULT_FLOW_EDITOR_LABELS.quickAdd, ...override.quickAdd },
|
|
357
|
+
legendPanel: { ...DEFAULT_FLOW_EDITOR_LABELS.legendPanel, ...override.legendPanel },
|
|
182
358
|
palette: { ...DEFAULT_FLOW_EDITOR_LABELS.palette, ...override.palette },
|
|
183
359
|
flowMap: { ...DEFAULT_FLOW_EDITOR_LABELS.flowMap, ...override.flowMap },
|
|
184
360
|
flowGroup: { ...DEFAULT_FLOW_EDITOR_LABELS.flowGroup, ...override.flowGroup },
|
|
185
361
|
crossFlowPortal: { ...DEFAULT_FLOW_EDITOR_LABELS.crossFlowPortal, ...override.crossFlowPortal },
|
|
362
|
+
collectionChain: { ...DEFAULT_FLOW_EDITOR_LABELS.collectionChain, ...override.collectionChain },
|
|
363
|
+
validation: { ...DEFAULT_FLOW_EDITOR_LABELS.validation, ...override.validation },
|
|
364
|
+
workspace: { ...DEFAULT_FLOW_EDITOR_LABELS.workspace, ...override.workspace },
|
|
365
|
+
flowManager: { ...DEFAULT_FLOW_EDITOR_LABELS.flowManager, ...override.flowManager },
|
|
186
366
|
}
|
|
187
367
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 Ada Technology. MIT License.
|
|
3
|
+
*
|
|
4
|
+
* O subpath `/flows` precisa exportar a TELA, não só as peças.
|
|
5
|
+
*
|
|
6
|
+
* Existe porque a ausência disso já custou: exportando apenas `FlowMapCanvas`, `FlowPalette` e
|
|
7
|
+
* `FlowNodePanel`, o financiamento montou a tela por conta — 973 linhas de página mais um fork local
|
|
8
|
+
* dos componentes, que ficou atrás do pacote. O quickcart, para ter a mesma tela, teria que copiar o
|
|
9
|
+
* arquivo. É exatamente a divergência que `pluggable-module.md` §4 proíbe.
|
|
10
|
+
*
|
|
11
|
+
* O teste é de superfície, não de comportamento: não prova que o canvas desenha certo, prova que
|
|
12
|
+
* existe UM lugar onde a tela mora, e que ela aceita customização por contrato em vez de por fork.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, expect, it } from 'bun:test'
|
|
16
|
+
|
|
17
|
+
import * as flows from './index'
|
|
18
|
+
import { DEFAULT_FLOW_EDITOR_LABELS, mergeFlowEditorLabels } from './labels'
|
|
19
|
+
|
|
20
|
+
const WORKSPACE_SOURCE = `${import.meta.dir}/FlowsWorkspace.tsx`
|
|
21
|
+
|
|
22
|
+
describe('superfície composta', () => {
|
|
23
|
+
it('exporta a tela inteira', () => {
|
|
24
|
+
expect(typeof flows.FlowsWorkspace).toBe('function')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('exporta as peças também — quem precisa de layout próprio não fica sem saída', () => {
|
|
28
|
+
// Workspace é o caminho recomendado, não uma prisão: um produto com layout radicalmente
|
|
29
|
+
// diferente compõe as peças, e isso é melhor que forkar o pacote.
|
|
30
|
+
for (const piece of ['FlowMapCanvas', 'FlowPalette', 'FlowNodePanel', 'FlowWhatsAppPreview']) {
|
|
31
|
+
expect(typeof (flows as Record<string, unknown>)[piece], piece).toBe('function')
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('as operações de grafo saem puras, sem passar pela tela', () => {
|
|
36
|
+
// São as que a tela consome de verdade (ver os imports em `FlowsWorkspace.tsx`). Exportá-las sem
|
|
37
|
+
// usá-las seria pior que não ter teste: teste verde sobre código que não roda em produção.
|
|
38
|
+
for (const operation of ['resolveConnection', 'applyConnection', 'removeNodeAndCleanRefs', 'mergedFlowKeysFrom']) {
|
|
39
|
+
expect(typeof (flows as Record<string, unknown>)[operation], operation).toBe('function')
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('contrato de customização', () => {
|
|
45
|
+
it('nenhuma capacidade em forma de flag booleana `hasX`', async () => {
|
|
46
|
+
/**
|
|
47
|
+
* Capacidade opcional é por AUSÊNCIA de prop. `hasDelete` seria um segundo jeito de dizer o que
|
|
48
|
+
* `deletableFlowKeys` já diz, e dois jeitos divergem — alguém liga a flag sem a lista e a tela
|
|
49
|
+
* desenha um botão que não exclui nada.
|
|
50
|
+
*/
|
|
51
|
+
const content = await Bun.file(WORKSPACE_SOURCE).text()
|
|
52
|
+
|
|
53
|
+
expect(content).not.toMatch(/readonly has[A-Z]/)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('aceita labels, className e os slots de render', async () => {
|
|
57
|
+
const content = await Bun.file(WORKSPACE_SOURCE).text()
|
|
58
|
+
|
|
59
|
+
expect(content).toContain('labels?: Partial<')
|
|
60
|
+
// `className` é o que deixa o produto posicionar a tela no layout dele sem tocar no pacote.
|
|
61
|
+
expect(content).toContain('className?: string')
|
|
62
|
+
expect(content).toContain('renderMediaPicker?:')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('nenhum texto visível escrito no componente — tudo passa por labels', async () => {
|
|
66
|
+
const content = await Bun.file(WORKSPACE_SOURCE).text()
|
|
67
|
+
/**
|
|
68
|
+
* Texto entre tags JSX que não seja `{...}`, que é o que `web.md` §6 proíbe.
|
|
69
|
+
*
|
|
70
|
+
* O `\s*` nas pontas não é detalhe: a primeira versão deste regex no notification-ui exigia o
|
|
71
|
+
* texto colado nas tags, e o Prettier põe o conteúdo em linha própria — o teste passava com
|
|
72
|
+
* `>\n Configurações\n<` no meio do componente, provando nada.
|
|
73
|
+
*/
|
|
74
|
+
const hardcoded = content.match(/>\s*[A-Za-zÀ-ÿ][A-Za-zÀ-ÿ ]{3,}\s*</g)
|
|
75
|
+
|
|
76
|
+
expect(hardcoded, `texto fixo: ${hardcoded?.join(' | ')}`).toBeNull()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('o produto sobrescreve UM texto sem perder os outros do mesmo grupo', () => {
|
|
80
|
+
const merged = mergeFlowEditorLabels({ workspace: { ...DEFAULT_FLOW_EDITOR_LABELS.workspace, title: 'Jornadas' } })
|
|
81
|
+
|
|
82
|
+
expect(merged.workspace.title).toBe('Jornadas')
|
|
83
|
+
expect(merged.workspace.saveGraph).toBe(DEFAULT_FLOW_EDITOR_LABELS.workspace.saveGraph)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('grupo novo de label entra no merge profundo', () => {
|
|
87
|
+
// Esquecer o grupo no `mergeFlowEditorLabels` deixa o override apagar os irmãos dele, e o
|
|
88
|
+
// sintoma é texto sumindo da tela — não erro.
|
|
89
|
+
for (const group of ['workspace', 'flowManager', 'validation', 'collectionChain'] as const) {
|
|
90
|
+
const merged = mergeFlowEditorLabels({ [group]: {} })
|
|
91
|
+
|
|
92
|
+
expect(Object.keys(merged[group]).length, group).toBe(Object.keys(DEFAULT_FLOW_EDITOR_LABELS[group]).length)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('focar um fluxo leva a câmera até ele', () => {
|
|
98
|
+
/**
|
|
99
|
+
* `focusFlow` trocava o fluxo primário e remesclava o canvas, mas nunca movia a viewport: a aba
|
|
100
|
+
* acendia, a tela continuava onde estava e o fluxo escolhido ficava fora do campo de visão — em
|
|
101
|
+
* "Consórcio", parado no início com o fluxo lá embaixo, clicar parecia não fazer nada.
|
|
102
|
+
*
|
|
103
|
+
* Teste de fonte, como os demais deste arquivo: não prova o enquadramento na tela, prova que o
|
|
104
|
+
* clique pede o enquadramento e que ele é restrito ao fluxo clicado.
|
|
105
|
+
*/
|
|
106
|
+
it('focar um fluxo agenda o enquadramento dele', async () => {
|
|
107
|
+
const content = await Bun.file(WORKSPACE_SOURCE).text()
|
|
108
|
+
|
|
109
|
+
expect(content).toContain('setPendingFocusFlowKey(key)')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('enquadra apenas os cards do fluxo focado, não o canvas inteiro', async () => {
|
|
113
|
+
// O canvas mostra o fecho transitivo inteiro. Um `fitView()` sem `nodes` devolveria a mesma
|
|
114
|
+
// visão de sempre — que é exatamente o defeito relatado.
|
|
115
|
+
const content = await Bun.file(WORKSPACE_SOURCE).text()
|
|
116
|
+
|
|
117
|
+
expect(content).toMatch(/fitView\(\{[\s\S]*?nodes: flowNodes\.map/)
|
|
118
|
+
expect(content).toMatch(/flowKey === pendingFocusFlowKey/)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('limita o zoom, para um fluxo de dois nós não encher a tela', async () => {
|
|
122
|
+
const content = await Bun.file(WORKSPACE_SOURCE).text()
|
|
123
|
+
|
|
124
|
+
expect(content).toContain('maxZoom: FOCUS_MAX_ZOOM')
|
|
125
|
+
})
|
|
126
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Largura do próprio elemento, para decidir layout pelo espaço que ele tem — e não pelo tamanho da
|
|
3
|
+
* janela.
|
|
4
|
+
*
|
|
5
|
+
* Os breakpoints do Tailwind (`sm:`, `lg:`) leem a janela, e é aí que erram nestas telas: abrir a
|
|
6
|
+
* prévia do simulador, ou a lista de conversas ao lado, estreita a coluna sem a janela mudar de
|
|
7
|
+
* tamanho. O layout continua achando que está no desktop e espreme o conteúdo flexível — o campo de
|
|
8
|
+
* texto do composer e o nome do cliente no cabeçalho — para caber os botões, que não cedem.
|
|
9
|
+
*
|
|
10
|
+
* `undefined` enquanto não mediu (SSR e teste de markup incluídos): quem consome trata ausência de
|
|
11
|
+
* medida como espaçoso, que é o layout completo.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useEffect, useState, type RefObject } from 'react'
|
|
15
|
+
|
|
16
|
+
export function useContainerWidth(ref: RefObject<HTMLElement | null>): number | undefined {
|
|
17
|
+
const [width, setWidth] = useState<number | undefined>(undefined)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const element = ref.current
|
|
21
|
+
if (!element || typeof ResizeObserver === 'undefined') return
|
|
22
|
+
|
|
23
|
+
// Sempre a mesma medida nos dois caminhos: o `contentRect` do observer desconta o padding e o
|
|
24
|
+
// `getBoundingClientRect` não. Misturar os dois desloca o limiar pela largura do padding — no
|
|
25
|
+
// cabeçalho, 32px — e o layout decide uma coisa ao montar e outra ao redimensionar.
|
|
26
|
+
const measure = () => setWidth(element.getBoundingClientRect().width)
|
|
27
|
+
|
|
28
|
+
measure()
|
|
29
|
+
const observer = new ResizeObserver(measure)
|
|
30
|
+
observer.observe(element)
|
|
31
|
+
return () => observer.disconnect()
|
|
32
|
+
}, [ref])
|
|
33
|
+
|
|
34
|
+
return width
|
|
35
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { useConversations } from '../providers/ConversationsProvider'
|
|
3
|
+
import type { ConversationTemplate } from '../providers/types'
|
|
4
|
+
|
|
5
|
+
export interface UseConversationActionsResult {
|
|
6
|
+
/** `undefined` quando a API do host não implementa a operação — a UI esconde a afordância. */
|
|
7
|
+
takeover: (() => Promise<void>) | undefined
|
|
8
|
+
release: (() => Promise<void>) | undefined
|
|
9
|
+
finalize: (() => Promise<void>) | undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Ações de atendimento de UMA conversa, já ligadas ao id.
|
|
14
|
+
*
|
|
15
|
+
* Separado de `useConversationMessages` porque assumir e devolver conversa também acontece a
|
|
16
|
+
* partir da lista, onde nenhuma thread está aberta — embutir nas mensagens obrigaria a carregar
|
|
17
|
+
* a thread inteira só para desenhar um botão na linha.
|
|
18
|
+
*/
|
|
19
|
+
export function useConversationActions(conversationId: string): UseConversationActionsResult {
|
|
20
|
+
const context = useConversations()
|
|
21
|
+
if (!context) {
|
|
22
|
+
throw new Error('useConversationActions requires an ancestor <ConversationsProvider>')
|
|
23
|
+
}
|
|
24
|
+
const { api } = context
|
|
25
|
+
|
|
26
|
+
return useMemo(
|
|
27
|
+
() => ({
|
|
28
|
+
takeover: api.takeover ? () => api.takeover!(conversationId) : undefined,
|
|
29
|
+
release: api.release ? () => api.release!(conversationId) : undefined,
|
|
30
|
+
finalize: api.finalize ? () => api.finalize!(conversationId) : undefined,
|
|
31
|
+
}),
|
|
32
|
+
[api, conversationId],
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UseInboxActionsResult {
|
|
37
|
+
markAllRead: (() => Promise<void>) | undefined
|
|
38
|
+
listTemplates: (() => Promise<ConversationTemplate[]>) | undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Ações que valem para a caixa inteira, sem conversa selecionada. */
|
|
42
|
+
export function useInboxActions(): UseInboxActionsResult {
|
|
43
|
+
const context = useConversations()
|
|
44
|
+
if (!context) {
|
|
45
|
+
throw new Error('useInboxActions requires an ancestor <ConversationsProvider>')
|
|
46
|
+
}
|
|
47
|
+
const { api } = context
|
|
48
|
+
|
|
49
|
+
return useMemo(
|
|
50
|
+
() => ({
|
|
51
|
+
markAllRead: api.markAllRead ? () => api.markAllRead!() : undefined,
|
|
52
|
+
listTemplates: api.listTemplates ? () => api.listTemplates!() : undefined,
|
|
53
|
+
}),
|
|
54
|
+
[api],
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { useConversations } from '../providers/ConversationsProvider'
|
|
2
2
|
import { useAsyncResource } from './useAsyncResource'
|
|
3
|
-
import
|
|
3
|
+
import { documentsOf, totalOf } from '../lib/paginated'
|
|
4
|
+
import type { ConversationDocument, ListDocumentsParams } from '../providers/types'
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
-
search?: string
|
|
7
|
-
page?: number
|
|
8
|
-
}
|
|
6
|
+
export type UseConversationDocumentsParams = ListDocumentsParams
|
|
9
7
|
|
|
10
8
|
export interface UseConversationDocumentsResult {
|
|
11
9
|
documents: ConversationDocument[]
|
|
10
|
+
/** Total no servidor. Cai para o tamanho da página quando a API devolve só o array. */
|
|
11
|
+
total: number
|
|
12
12
|
loading: boolean
|
|
13
13
|
error: Error | undefined
|
|
14
14
|
refetch: () => Promise<void>
|
|
@@ -28,8 +28,12 @@ export function useConversationDocuments(
|
|
|
28
28
|
|
|
29
29
|
const { data, loading, error, refetch } = useAsyncResource(
|
|
30
30
|
() => (conversationId ? api.getDocuments(conversationId, params) : Promise.resolve([])),
|
|
31
|
-
[conversationId, params?.search, params?.page],
|
|
31
|
+
[conversationId, params?.search, params?.page, params?.limit, params?.source, params?.sortDirection],
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
if (data === undefined) {
|
|
35
|
+
return { documents: [], total: 0, loading, error, refetch }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { documents: documentsOf(data), total: totalOf(data), loading, error, refetch }
|
|
35
39
|
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { useConversations } from '../providers/ConversationsProvider'
|
|
2
2
|
import { useAsyncResource } from './useAsyncResource'
|
|
3
|
-
import
|
|
3
|
+
import { conversationsOf, totalOf } from '../lib/paginated'
|
|
4
|
+
import type { ConversationSummary, ListConversationsParams } from '../providers/types'
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
-
page?: number
|
|
7
|
-
limit?: number
|
|
8
|
-
waitingHuman?: boolean
|
|
9
|
-
search?: string
|
|
10
|
-
}
|
|
6
|
+
export type UseConversationListParams = ListConversationsParams
|
|
11
7
|
|
|
12
8
|
export interface UseConversationListResult {
|
|
13
9
|
conversations: ConversationSummary[]
|
|
10
|
+
/** Total no servidor. Cai para o tamanho da página quando a API devolve só o array. */
|
|
11
|
+
total: number
|
|
14
12
|
loading: boolean
|
|
15
13
|
error: Error | undefined
|
|
16
14
|
refetch: () => Promise<void>
|
|
@@ -23,10 +21,18 @@ export function useConversationList(params?: UseConversationListParams): UseConv
|
|
|
23
21
|
}
|
|
24
22
|
const { api } = context
|
|
25
23
|
|
|
24
|
+
// `filters` é objeto novo a cada render do host; serializar evita refetch em laço sem obrigar
|
|
25
|
+
// o consumidor a memoizar — omissão que só apareceria como loop de rede em produção.
|
|
26
|
+
const filtersKey = JSON.stringify(params?.filters ?? {})
|
|
27
|
+
|
|
26
28
|
const { data, loading, error, refetch } = useAsyncResource(
|
|
27
29
|
() => api.fetchConversations(params),
|
|
28
|
-
[params?.page, params?.limit, params?.waitingHuman, params?.search],
|
|
30
|
+
[params?.page, params?.limit, params?.waitingHuman, params?.search, filtersKey],
|
|
29
31
|
)
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
if (data === undefined) {
|
|
34
|
+
return { conversations: [], total: 0, loading, error, refetch }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { conversations: conversationsOf(data), total: totalOf(data), loading, error, refetch }
|
|
32
38
|
}
|
|
@@ -10,7 +10,7 @@ export interface UseConversationMessagesResult {
|
|
|
10
10
|
refetch: () => Promise<void>
|
|
11
11
|
sendMessage: (text: string) => Promise<MessagePayload>
|
|
12
12
|
sendMedia: (data: { base64: string; mimeType: string; filename: string; caption?: string }) => Promise<MessagePayload>
|
|
13
|
-
sendTemplate: (data: { templateName
|
|
13
|
+
sendTemplate: (data: { templateName?: string; languageCode?: string; bodyParams?: string[] }) => Promise<void>
|
|
14
14
|
markRead: () => Promise<void>
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -51,7 +51,7 @@ export function useConversationMessages(
|
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
const sendTemplate = useCallback(
|
|
54
|
-
async (templateData: { templateName
|
|
54
|
+
async (templateData: { templateName?: string; languageCode?: string; bodyParams?: string[] }) => {
|
|
55
55
|
await api.sendTemplate(conversationId, templateData)
|
|
56
56
|
await refetch()
|
|
57
57
|
},
|
|
@@ -10,14 +10,16 @@ export function useConversationRealtime(
|
|
|
10
10
|
conversationId: string | undefined,
|
|
11
11
|
onEvent: ConversationRealtimeHandler,
|
|
12
12
|
): void {
|
|
13
|
-
const
|
|
13
|
+
const sse = useConversations()?.sse
|
|
14
14
|
const onEventRef = useRef(onEvent)
|
|
15
15
|
onEventRef.current = onEvent
|
|
16
16
|
|
|
17
|
+
// Depende da porta SSE, não do objeto de contexto inteiro: reabrir o stream é caro (um ticket por
|
|
18
|
+
// abertura) e o contexto é o que mais muda de identidade quando o host renderiza.
|
|
17
19
|
useEffect(() => {
|
|
18
|
-
if (!
|
|
20
|
+
if (!sse || !conversationId) return
|
|
19
21
|
|
|
20
|
-
const source =
|
|
22
|
+
const source = sse.connectConversationStream(conversationId)
|
|
21
23
|
const handler = (event: MessageEvent) => onEventRef.current(event)
|
|
22
24
|
source.addEventListener('message', handler)
|
|
23
25
|
|
|
@@ -25,20 +27,20 @@ export function useConversationRealtime(
|
|
|
25
27
|
source.removeEventListener('message', handler)
|
|
26
28
|
source.close()
|
|
27
29
|
}
|
|
28
|
-
}, [
|
|
30
|
+
}, [sse, conversationId])
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
// Assina o stream global (ex: novas conversas entrando na fila, notificações cross-conversa)
|
|
32
34
|
// — mesma porta SSEProvider, sem conversationId.
|
|
33
35
|
export function useGlobalRealtime(onEvent: ConversationRealtimeHandler): void {
|
|
34
|
-
const
|
|
36
|
+
const sse = useConversations()?.sse
|
|
35
37
|
const onEventRef = useRef(onEvent)
|
|
36
38
|
onEventRef.current = onEvent
|
|
37
39
|
|
|
38
40
|
useEffect(() => {
|
|
39
|
-
if (!
|
|
41
|
+
if (!sse) return
|
|
40
42
|
|
|
41
|
-
const source =
|
|
43
|
+
const source = sse.connectGlobalStream()
|
|
42
44
|
const handler = (event: MessageEvent) => onEventRef.current(event)
|
|
43
45
|
source.addEventListener('message', handler)
|
|
44
46
|
|
|
@@ -46,5 +48,5 @@ export function useGlobalRealtime(onEvent: ConversationRealtimeHandler): void {
|
|
|
46
48
|
source.removeEventListener('message', handler)
|
|
47
49
|
source.close()
|
|
48
50
|
}
|
|
49
|
-
}, [
|
|
51
|
+
}, [sse])
|
|
50
52
|
}
|