@adatechnology/conversations-ui 0.1.0-rc.8 → 0.1.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--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 +2502 -678
- package/dist/index.d.ts +921 -17
- package/dist/index.js +3677 -678
- package/dist/preview/index.d.ts +62 -105
- package/dist/preview/index.js +162 -284
- 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 +1 -1
- package/src/ConversationContextPanel.tsx +218 -44
- package/src/ConversationDocumentsPanel.tsx +11 -6
- package/src/ConversationHeader.test.tsx +66 -0
- package/src/ConversationHeader.tsx +147 -47
- package/src/ConversationListItem.tsx +8 -6
- package/src/ConversationLocalesProvider.tsx +28 -0
- package/src/ConversationRow.tsx +53 -7
- package/src/DarkModeToggle.test.tsx +76 -0
- package/src/DarkModeToggle.tsx +92 -0
- package/src/DocumentsLibrary.tsx +67 -7
- package/src/EmojiPicker.tsx +2 -1
- package/src/InteractiveMessage.tsx +3 -0
- package/src/Lightbox.tsx +1 -1
- package/src/MediaRenderer.tsx +88 -15
- package/src/MessageBubble.test.tsx +41 -0
- package/src/MessageBubble.tsx +47 -5
- package/src/MessageComposer.test.tsx +35 -0
- package/src/MessageComposer.tsx +122 -17
- package/src/MessageText.tsx +2 -1
- package/src/MessageTimestamp.tsx +2 -1
- 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 +10 -7
- package/src/WindowExpiredNotice.tsx +12 -4
- package/src/{preview/audioRecorderFormat.test.ts → audioRecorderFormat.test.ts} +1 -1
- 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/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 +156 -70
- package/src/flows/FlowPortalNode.tsx +1 -1
- package/src/flows/FlowWhatsAppPreview.tsx +14 -3
- package/src/flows/FlowsWorkspace.tsx +1255 -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/flowMenuPlacement.test.ts +130 -0
- package/src/flows/flowMenuPlacement.ts +86 -0
- 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/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 +100 -0
- package/src/lib/composer-formatting.test.ts +78 -0
- package/src/lib/composer-formatting.ts +145 -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 +84 -45
- 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/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 +99 -3
- package/src/preview/index.ts +36 -2
- package/src/preview/previewMediaUploader.test.ts +61 -0
- package/src/providers/ConversationsProvider.tsx +8 -6
- package/src/providers/types.ts +59 -2
- package/src/quickReply.test.ts +58 -0
- package/src/replyLatency.test.ts +71 -0
- package/src/replyLatency.ts +57 -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 +1 -0
- 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/theme.ts +13 -0
- package/src/types.ts +26 -0
- 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-73MW5HNT.js +0 -1717
- package/dist/chunk-NV2RZ5KT.js +0 -56
- package/dist/types-B5C1DLu1.d.ts +0 -365
- package/src/preview/AudioRecorderButton.tsx +0 -117
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tela completa de "Mensagens": mensagens do bot, tópicos, templates do WhatsApp e transcrição.
|
|
3
|
+
*
|
|
4
|
+
* Os formulários daqui já eram do pacote, mas cada produto remontava as abas, o estado e o
|
|
5
|
+
* salvamento por conta — e foi exatamente aí que divergiram (o financiamento tinha tópicos e
|
|
6
|
+
* criação de template, o QuickCart tinha transcrição, e nenhum dos dois tinha tudo). Esta tela
|
|
7
|
+
* é a composição: recebe funções assíncronas e devolve a tela inteira.
|
|
8
|
+
*
|
|
9
|
+
* Tudo além das mensagens de boas-vindas/despedida é **opcional por capacidade**: sem
|
|
10
|
+
* `getTopics`, a seção de tópicos não existe; sem `listTemplates`, a aba de templates não
|
|
11
|
+
* aparece; sem `createTemplate`, o submenu de criação some. Nada de aba que abre em branco.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useCallback, useEffect, useState, type FormEvent, type ReactNode } from 'react'
|
|
15
|
+
import { WelcomeFarewellForm, type WelcomeFarewellFormLabels } from './WelcomeFarewellForm'
|
|
16
|
+
import { TopicsForm, type TopicItem, type TopicsFormLabels } from './TopicsForm'
|
|
17
|
+
import {
|
|
18
|
+
WhatsAppTemplatesSettings,
|
|
19
|
+
type WhatsAppTemplatesSettingsLabels,
|
|
20
|
+
} from './WhatsAppTemplatesSettings'
|
|
21
|
+
import {
|
|
22
|
+
WhatsAppTemplateSettingsForm,
|
|
23
|
+
type WhatsAppTemplateSummary,
|
|
24
|
+
type WhatsAppTemplateVariableSuggestion,
|
|
25
|
+
type WhatsAppTemplateSettingsFormLabels,
|
|
26
|
+
} from './WhatsAppTemplateSettingsForm'
|
|
27
|
+
import type {
|
|
28
|
+
WhatsAppCreateTemplateFormLabels,
|
|
29
|
+
WhatsAppCreateTemplateResult,
|
|
30
|
+
WhatsAppCreateTemplateState,
|
|
31
|
+
} from './WhatsAppCreateTemplateForm'
|
|
32
|
+
import {
|
|
33
|
+
TranscriptionSettingsForm,
|
|
34
|
+
type TranscriptionSettingsFormLabels,
|
|
35
|
+
} from './TranscriptionSettingsForm'
|
|
36
|
+
import type { TranscriptionMode } from '../types'
|
|
37
|
+
import { TooltipLayer } from '../Tooltip'
|
|
38
|
+
|
|
39
|
+
export interface BotMessages {
|
|
40
|
+
welcomeMessage: string
|
|
41
|
+
farewellMessage: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TemplateSettings {
|
|
45
|
+
templateName: string
|
|
46
|
+
templateLanguage: string
|
|
47
|
+
variables: string[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Papel adicional de template (ex.: despedida) além do principal (`getTemplateSettings`/
|
|
52
|
+
* `saveTemplateSettings`). Cada papel ganha seu próprio formulário empilhado na mesma aba, com
|
|
53
|
+
* carregamento e salvamento independentes — é o que permite hosts com mais de um papel (o
|
|
54
|
+
* financiamento tem boas-vindas + despedida) convergirem para este componente em vez de remontar
|
|
55
|
+
* a tela à parte.
|
|
56
|
+
*/
|
|
57
|
+
export interface MessagesWorkspaceTemplateRole {
|
|
58
|
+
readonly key: string
|
|
59
|
+
readonly labels: { sectionTitle: string; sectionDescription: string }
|
|
60
|
+
getSettings(): Promise<TemplateSettings>
|
|
61
|
+
saveSettings(settings: TemplateSettings): Promise<void>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface TranscriptionSettings {
|
|
65
|
+
enabled: boolean
|
|
66
|
+
mode: TranscriptionMode
|
|
67
|
+
available: boolean
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface MessagesWorkspaceApi {
|
|
71
|
+
getMessages(): Promise<BotMessages>
|
|
72
|
+
saveMessages(messages: BotMessages): Promise<void>
|
|
73
|
+
/** Sem este par, a seção de tópicos não é desenhada. */
|
|
74
|
+
getTopics?(): Promise<TopicItem[]>
|
|
75
|
+
saveTopics?(topics: TopicItem[]): Promise<void>
|
|
76
|
+
/** Sem este par, a aba de templates não é desenhada. */
|
|
77
|
+
getTemplateSettings?(): Promise<TemplateSettings>
|
|
78
|
+
saveTemplateSettings?(settings: TemplateSettings): Promise<void>
|
|
79
|
+
/** Papéis adicionais de template (ex.: despedida) além do principal acima. */
|
|
80
|
+
templateRoles?: MessagesWorkspaceTemplateRole[]
|
|
81
|
+
/**
|
|
82
|
+
* Lista de templates aprovados na Meta. Ausente, a aba ainda existe (dá para salvar o nome
|
|
83
|
+
* escolhido), mas nasce sem opções e sem botão de recarregar.
|
|
84
|
+
*/
|
|
85
|
+
listTemplates?(): Promise<WhatsAppTemplateSummary[]>
|
|
86
|
+
/** Sem isto, o submenu "Criar template" some. */
|
|
87
|
+
createTemplate?(input: WhatsAppCreateTemplateState): Promise<WhatsAppCreateTemplateResult>
|
|
88
|
+
/** Sem este par, a aba de transcrição não é desenhada. */
|
|
89
|
+
getTranscription?(): Promise<TranscriptionSettings>
|
|
90
|
+
saveTranscription?(settings: Omit<TranscriptionSettings, 'available'>): Promise<void>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface MessagesWorkspaceLabels {
|
|
94
|
+
title: string
|
|
95
|
+
subtitle: string
|
|
96
|
+
loading: string
|
|
97
|
+
loadError: string
|
|
98
|
+
tabBot: string
|
|
99
|
+
tabTemplates: string
|
|
100
|
+
tabTranscription: string
|
|
101
|
+
welcomeFarewell: Partial<WelcomeFarewellFormLabels>
|
|
102
|
+
topics: Partial<TopicsFormLabels>
|
|
103
|
+
templates: Partial<WhatsAppTemplatesSettingsLabels>
|
|
104
|
+
templateSettings: Partial<WhatsAppTemplateSettingsFormLabels>
|
|
105
|
+
createTemplate: Partial<WhatsAppCreateTemplateFormLabels>
|
|
106
|
+
transcription: Partial<TranscriptionSettingsFormLabels>
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const DEFAULT_LABELS: MessagesWorkspaceLabels = {
|
|
110
|
+
title: 'Mensagens',
|
|
111
|
+
subtitle: 'Mensagens do bot e templates do WhatsApp.',
|
|
112
|
+
loading: 'Carregando configurações…',
|
|
113
|
+
loadError: 'Não foi possível carregar as configurações de mensagens.',
|
|
114
|
+
tabBot: 'Mensagens do Bot',
|
|
115
|
+
tabTemplates: 'Templates WhatsApp',
|
|
116
|
+
tabTranscription: 'Transcrição de áudio',
|
|
117
|
+
welcomeFarewell: {},
|
|
118
|
+
topics: {},
|
|
119
|
+
templates: {},
|
|
120
|
+
templateSettings: {},
|
|
121
|
+
createTemplate: {},
|
|
122
|
+
transcription: {},
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const MESSAGES_TAB = {
|
|
126
|
+
BOT: 'bot',
|
|
127
|
+
TEMPLATES: 'templates',
|
|
128
|
+
TRANSCRIPTION: 'transcription',
|
|
129
|
+
} as const
|
|
130
|
+
type MessagesTab = (typeof MESSAGES_TAB)[keyof typeof MESSAGES_TAB]
|
|
131
|
+
|
|
132
|
+
const EMPTY_MESSAGES: BotMessages = { welcomeMessage: '', farewellMessage: '' }
|
|
133
|
+
const EMPTY_TEMPLATE_SETTINGS: TemplateSettings = {
|
|
134
|
+
templateName: '',
|
|
135
|
+
templateLanguage: 'pt_BR',
|
|
136
|
+
variables: [],
|
|
137
|
+
}
|
|
138
|
+
const EMPTY_CREATE_TEMPLATE: WhatsAppCreateTemplateState = {
|
|
139
|
+
name: '',
|
|
140
|
+
category: 'UTILITY',
|
|
141
|
+
language: 'pt_BR',
|
|
142
|
+
headerType: 'NONE',
|
|
143
|
+
headerText: '',
|
|
144
|
+
bodyText: '',
|
|
145
|
+
footerText: '',
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Padrão da instalação quando ninguém decidiu ainda.
|
|
150
|
+
*
|
|
151
|
+
* O servidor guarda `null` de propósito (herda o ambiente), mas o interruptor é booleano —
|
|
152
|
+
* `false` é a leitura honesta de "ninguém ligou aqui"; marcado sugeriria escolha que não houve.
|
|
153
|
+
*/
|
|
154
|
+
const UNDECIDED_TRANSCRIPTION: TranscriptionSettings = {
|
|
155
|
+
enabled: false,
|
|
156
|
+
mode: 'onDemand',
|
|
157
|
+
available: false,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const SAVE_FEEDBACK_MS = 3000
|
|
161
|
+
|
|
162
|
+
export interface MessagesWorkspaceProps {
|
|
163
|
+
readonly api: MessagesWorkspaceApi
|
|
164
|
+
readonly labels?: Partial<MessagesWorkspaceLabels>
|
|
165
|
+
readonly welcomePlaceholders?: readonly string[]
|
|
166
|
+
readonly farewellPlaceholders?: readonly string[]
|
|
167
|
+
readonly availableVariables?: WhatsAppTemplateVariableSuggestion[]
|
|
168
|
+
/** Aviso do produto acima da aba de templates (ex.: rota da Graph API ainda não implementada). */
|
|
169
|
+
readonly renderTemplatesNotice?: () => ReactNode
|
|
170
|
+
/** Repassados ao formulário de criação — mesma prévia que `WhatsAppCreateTemplateForm` já suporta. */
|
|
171
|
+
readonly createTemplatePreviewCompanyName?: string
|
|
172
|
+
readonly createTemplateVariableExamples?: readonly string[]
|
|
173
|
+
readonly className?: string
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function MessagesWorkspace({
|
|
177
|
+
api,
|
|
178
|
+
labels: labelsOverride,
|
|
179
|
+
welcomePlaceholders,
|
|
180
|
+
farewellPlaceholders,
|
|
181
|
+
availableVariables,
|
|
182
|
+
renderTemplatesNotice,
|
|
183
|
+
createTemplatePreviewCompanyName,
|
|
184
|
+
createTemplateVariableExamples,
|
|
185
|
+
className,
|
|
186
|
+
}: MessagesWorkspaceProps) {
|
|
187
|
+
const labels = { ...DEFAULT_LABELS, ...labelsOverride }
|
|
188
|
+
const templateRoles = api.templateRoles ?? []
|
|
189
|
+
|
|
190
|
+
const hasTopics = Boolean(api.getTopics && api.saveTopics)
|
|
191
|
+
const hasTemplates = Boolean(api.getTemplateSettings && api.saveTemplateSettings)
|
|
192
|
+
const hasTranscription = Boolean(api.getTranscription && api.saveTranscription)
|
|
193
|
+
|
|
194
|
+
const [tab, setTab] = useState<MessagesTab>(MESSAGES_TAB.BOT)
|
|
195
|
+
const [loadState, setLoadState] = useState<'loading' | 'ready' | 'error'>('loading')
|
|
196
|
+
|
|
197
|
+
const [messages, setMessages] = useState<BotMessages>(EMPTY_MESSAGES)
|
|
198
|
+
const [savingMessages, setSavingMessages] = useState(false)
|
|
199
|
+
const [messagesSaved, setMessagesSaved] = useState(false)
|
|
200
|
+
|
|
201
|
+
const [topics, setTopics] = useState<TopicItem[]>([])
|
|
202
|
+
const [savingTopics, setSavingTopics] = useState(false)
|
|
203
|
+
const [topicsSaved, setTopicsSaved] = useState(false)
|
|
204
|
+
|
|
205
|
+
const [templateSettings, setTemplateSettings] = useState<TemplateSettings>(EMPTY_TEMPLATE_SETTINGS)
|
|
206
|
+
const [templates, setTemplates] = useState<WhatsAppTemplateSummary[]>([])
|
|
207
|
+
const [loadingTemplates, setLoadingTemplates] = useState(false)
|
|
208
|
+
const [templatesError, setTemplatesError] = useState(false)
|
|
209
|
+
const [savingTemplate, setSavingTemplate] = useState(false)
|
|
210
|
+
const [templateSaved, setTemplateSaved] = useState(false)
|
|
211
|
+
|
|
212
|
+
const [roleSettings, setRoleSettings] = useState<Record<string, TemplateSettings>>({})
|
|
213
|
+
const [savingRole, setSavingRole] = useState<Record<string, boolean>>({})
|
|
214
|
+
const [roleSaved, setRoleSaved] = useState<Record<string, boolean>>({})
|
|
215
|
+
|
|
216
|
+
const [createTemplate, setCreateTemplate] = useState<WhatsAppCreateTemplateState>(EMPTY_CREATE_TEMPLATE)
|
|
217
|
+
const [creatingTemplate, setCreatingTemplate] = useState(false)
|
|
218
|
+
const [createResult, setCreateResult] = useState<WhatsAppCreateTemplateResult | null>(null)
|
|
219
|
+
|
|
220
|
+
const [transcription, setTranscription] = useState<TranscriptionSettings>(UNDECIDED_TRANSCRIPTION)
|
|
221
|
+
const [savingTranscription, setSavingTranscription] = useState(false)
|
|
222
|
+
const [transcriptionSaved, setTranscriptionSaved] = useState(false)
|
|
223
|
+
|
|
224
|
+
const reloadTemplates = useCallback(async () => {
|
|
225
|
+
if (!api.listTemplates) return
|
|
226
|
+
setLoadingTemplates(true)
|
|
227
|
+
setTemplatesError(false)
|
|
228
|
+
try {
|
|
229
|
+
setTemplates(await api.listTemplates())
|
|
230
|
+
} catch {
|
|
231
|
+
setTemplatesError(true)
|
|
232
|
+
} finally {
|
|
233
|
+
setLoadingTemplates(false)
|
|
234
|
+
}
|
|
235
|
+
}, [api])
|
|
236
|
+
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
let active = true
|
|
239
|
+
async function load(): Promise<void> {
|
|
240
|
+
try {
|
|
241
|
+
const roles = api.templateRoles ?? []
|
|
242
|
+
const [loadedMessages, loadedTopics, loadedTemplateSettings, loadedTranscription, loadedRoleSettings] =
|
|
243
|
+
await Promise.all([
|
|
244
|
+
api.getMessages(),
|
|
245
|
+
api.getTopics?.(),
|
|
246
|
+
api.getTemplateSettings?.(),
|
|
247
|
+
api.getTranscription?.(),
|
|
248
|
+
Promise.all(roles.map((role) => role.getSettings())),
|
|
249
|
+
])
|
|
250
|
+
if (!active) return
|
|
251
|
+
setMessages(loadedMessages)
|
|
252
|
+
if (loadedTopics) setTopics(loadedTopics)
|
|
253
|
+
if (loadedTemplateSettings) setTemplateSettings(loadedTemplateSettings)
|
|
254
|
+
if (loadedTranscription) setTranscription(loadedTranscription)
|
|
255
|
+
setRoleSettings(Object.fromEntries(roles.map((role, index) => [role.key, loadedRoleSettings[index]])))
|
|
256
|
+
setLoadState('ready')
|
|
257
|
+
} catch {
|
|
258
|
+
if (active) setLoadState('error')
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
void load()
|
|
262
|
+
return () => {
|
|
263
|
+
active = false
|
|
264
|
+
}
|
|
265
|
+
}, [api])
|
|
266
|
+
|
|
267
|
+
// A lista de templates é buscada à parte porque falha sozinha: a Graph API cai, ou a rota nem
|
|
268
|
+
// existe no produto, e nada disso pode impedir a tela de abrir para salvar o resto.
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
void reloadTemplates()
|
|
271
|
+
}, [reloadTemplates])
|
|
272
|
+
|
|
273
|
+
function flash(setter: (value: boolean) => void): void {
|
|
274
|
+
setter(true)
|
|
275
|
+
setTimeout(() => setter(false), SAVE_FEEDBACK_MS)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function handleSaveMessages(event: FormEvent): Promise<void> {
|
|
279
|
+
event.preventDefault()
|
|
280
|
+
setSavingMessages(true)
|
|
281
|
+
try {
|
|
282
|
+
await api.saveMessages(messages)
|
|
283
|
+
flash(setMessagesSaved)
|
|
284
|
+
} finally {
|
|
285
|
+
setSavingMessages(false)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function handleSaveTopics(event: FormEvent): Promise<void> {
|
|
290
|
+
event.preventDefault()
|
|
291
|
+
if (!api.saveTopics) return
|
|
292
|
+
setSavingTopics(true)
|
|
293
|
+
try {
|
|
294
|
+
await api.saveTopics(topics)
|
|
295
|
+
flash(setTopicsSaved)
|
|
296
|
+
} finally {
|
|
297
|
+
setSavingTopics(false)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function handleSaveTemplate(event: FormEvent): Promise<void> {
|
|
302
|
+
event.preventDefault()
|
|
303
|
+
if (!api.saveTemplateSettings) return
|
|
304
|
+
setSavingTemplate(true)
|
|
305
|
+
try {
|
|
306
|
+
await api.saveTemplateSettings(templateSettings)
|
|
307
|
+
flash(setTemplateSaved)
|
|
308
|
+
} finally {
|
|
309
|
+
setSavingTemplate(false)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function handleSaveRole(role: MessagesWorkspaceTemplateRole): (event: FormEvent) => Promise<void> {
|
|
314
|
+
return async (event: FormEvent) => {
|
|
315
|
+
event.preventDefault()
|
|
316
|
+
const settings = roleSettings[role.key]
|
|
317
|
+
if (!settings) return
|
|
318
|
+
setSavingRole((previous) => ({ ...previous, [role.key]: true }))
|
|
319
|
+
try {
|
|
320
|
+
await role.saveSettings(settings)
|
|
321
|
+
setRoleSaved((previous) => ({ ...previous, [role.key]: true }))
|
|
322
|
+
setTimeout(() => setRoleSaved((previous) => ({ ...previous, [role.key]: false })), SAVE_FEEDBACK_MS)
|
|
323
|
+
} finally {
|
|
324
|
+
setSavingRole((previous) => ({ ...previous, [role.key]: false }))
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function handleSelectRoleTemplate(
|
|
330
|
+
role: MessagesWorkspaceTemplateRole,
|
|
331
|
+
name: string,
|
|
332
|
+
template: WhatsAppTemplateSummary | undefined,
|
|
333
|
+
): void {
|
|
334
|
+
setRoleSettings((previous) => {
|
|
335
|
+
const current = previous[role.key] ?? EMPTY_TEMPLATE_SETTINGS
|
|
336
|
+
if (!template) return { ...previous, [role.key]: { ...current, templateName: name } }
|
|
337
|
+
const shouldSeedVariables = template.variableCount > 0 && current.variables.length === 0
|
|
338
|
+
return {
|
|
339
|
+
...previous,
|
|
340
|
+
[role.key]: {
|
|
341
|
+
templateName: name,
|
|
342
|
+
templateLanguage: template.language,
|
|
343
|
+
variables: shouldSeedVariables ? Array.from({ length: template.variableCount }, () => '') : current.variables,
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function handleCreateTemplate(event: FormEvent): Promise<void> {
|
|
350
|
+
event.preventDefault()
|
|
351
|
+
if (!api.createTemplate) return
|
|
352
|
+
setCreatingTemplate(true)
|
|
353
|
+
setCreateResult(null)
|
|
354
|
+
try {
|
|
355
|
+
const result = await api.createTemplate(createTemplate)
|
|
356
|
+
setCreateResult(result)
|
|
357
|
+
if (result.ok) {
|
|
358
|
+
setCreateTemplate(EMPTY_CREATE_TEMPLATE)
|
|
359
|
+
void reloadTemplates()
|
|
360
|
+
}
|
|
361
|
+
} catch (error) {
|
|
362
|
+
setCreateResult({ ok: false, message: error instanceof Error ? error.message : '' })
|
|
363
|
+
} finally {
|
|
364
|
+
setCreatingTemplate(false)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function handleSaveTranscription(event: FormEvent): Promise<void> {
|
|
369
|
+
event.preventDefault()
|
|
370
|
+
if (!api.saveTranscription) return
|
|
371
|
+
setSavingTranscription(true)
|
|
372
|
+
try {
|
|
373
|
+
await api.saveTranscription({ enabled: transcription.enabled, mode: transcription.mode })
|
|
374
|
+
flash(setTranscriptionSaved)
|
|
375
|
+
} finally {
|
|
376
|
+
setSavingTranscription(false)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function handleSelectTemplate(name: string, template: WhatsAppTemplateSummary | undefined): void {
|
|
381
|
+
setTemplateSettings((previous) => {
|
|
382
|
+
if (!template) return { ...previous, templateName: name }
|
|
383
|
+
// Escolher um template com variáveis e deixar a lista vazia é o erro clássico: o envio
|
|
384
|
+
// vai à Meta sem os parâmetros e volta rejeitado. Pré-abrir os campos evita isso — mas só
|
|
385
|
+
// quando ninguém preencheu nada ainda, para não apagar mapeamento existente.
|
|
386
|
+
const shouldSeedVariables = template.variableCount > 0 && previous.variables.length === 0
|
|
387
|
+
return {
|
|
388
|
+
templateName: name,
|
|
389
|
+
templateLanguage: template.language,
|
|
390
|
+
variables: shouldSeedVariables
|
|
391
|
+
? Array.from({ length: template.variableCount }, () => '')
|
|
392
|
+
: previous.variables,
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (loadState === 'loading') {
|
|
398
|
+
return <p className="p-6 text-sm text-gray-500 dark:text-gray-400">{labels.loading}</p>
|
|
399
|
+
}
|
|
400
|
+
if (loadState === 'error') {
|
|
401
|
+
return (
|
|
402
|
+
<p role="alert" className="p-6 text-sm text-red-600 dark:text-red-400">
|
|
403
|
+
{labels.loadError}
|
|
404
|
+
</p>
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const tabs: { id: MessagesTab; label: string }[] = [
|
|
409
|
+
{ id: MESSAGES_TAB.BOT, label: labels.tabBot },
|
|
410
|
+
...(hasTemplates ? [{ id: MESSAGES_TAB.TEMPLATES, label: labels.tabTemplates }] : []),
|
|
411
|
+
...(hasTranscription ? [{ id: MESSAGES_TAB.TRANSCRIPTION, label: labels.tabTranscription }] : []),
|
|
412
|
+
]
|
|
413
|
+
|
|
414
|
+
return (
|
|
415
|
+
<div className={`space-y-6 p-4 lg:p-0 ${className ?? ''}`}>
|
|
416
|
+
<TooltipLayer />
|
|
417
|
+
<header>
|
|
418
|
+
<h1 className="text-xl font-semibold text-gray-900 dark:text-gray-100">{labels.title}</h1>
|
|
419
|
+
<p className="text-sm text-gray-500 dark:text-gray-400">{labels.subtitle}</p>
|
|
420
|
+
</header>
|
|
421
|
+
|
|
422
|
+
{/* Uma aba só não é navegação: vira ruído com aparência de escolha. */}
|
|
423
|
+
{tabs.length > 1 ? (
|
|
424
|
+
<nav className="flex gap-1 border-b border-gray-200 dark:border-gray-700" role="tablist">
|
|
425
|
+
{tabs.map((item) => (
|
|
426
|
+
<button
|
|
427
|
+
data-cv-tooltip={item.label} aria-label={item.label}
|
|
428
|
+
key={item.id}
|
|
429
|
+
type="button"
|
|
430
|
+
role="tab"
|
|
431
|
+
aria-selected={tab === item.id}
|
|
432
|
+
onClick={() => setTab(item.id)}
|
|
433
|
+
className={`-mb-px border-b-2 px-4 py-2.5 text-sm font-medium transition-colors ${
|
|
434
|
+
tab === item.id
|
|
435
|
+
? 'border-blue-600 text-blue-700 dark:border-blue-400 dark:text-blue-400'
|
|
436
|
+
: 'border-transparent text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200'
|
|
437
|
+
}`}
|
|
438
|
+
>
|
|
439
|
+
{item.label}
|
|
440
|
+
</button>
|
|
441
|
+
))}
|
|
442
|
+
</nav>
|
|
443
|
+
) : null}
|
|
444
|
+
|
|
445
|
+
{tab === MESSAGES_TAB.BOT ? (
|
|
446
|
+
<div className="space-y-8">
|
|
447
|
+
<WelcomeFarewellForm
|
|
448
|
+
welcomeMessage={messages.welcomeMessage}
|
|
449
|
+
onWelcomeMessageChange={(welcomeMessage) =>
|
|
450
|
+
setMessages((previous) => ({ ...previous, welcomeMessage }))
|
|
451
|
+
}
|
|
452
|
+
farewellMessage={messages.farewellMessage}
|
|
453
|
+
onFarewellMessageChange={(farewellMessage) =>
|
|
454
|
+
setMessages((previous) => ({ ...previous, farewellMessage }))
|
|
455
|
+
}
|
|
456
|
+
onSave={handleSaveMessages}
|
|
457
|
+
saving={savingMessages}
|
|
458
|
+
saveSuccess={messagesSaved}
|
|
459
|
+
labels={labels.welcomeFarewell}
|
|
460
|
+
{...(welcomePlaceholders ? { welcomePlaceholders } : {})}
|
|
461
|
+
{...(farewellPlaceholders ? { farewellPlaceholders } : {})}
|
|
462
|
+
/>
|
|
463
|
+
|
|
464
|
+
{hasTopics ? (
|
|
465
|
+
<TopicsForm
|
|
466
|
+
topics={topics}
|
|
467
|
+
onToggle={(key) =>
|
|
468
|
+
setTopics((previous) =>
|
|
469
|
+
previous.map((topic) => (topic.key === key ? { ...topic, enabled: !topic.enabled } : topic)),
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
onMessageChange={(key, message) =>
|
|
473
|
+
setTopics((previous) =>
|
|
474
|
+
previous.map((topic) => (topic.key === key ? { ...topic, message } : topic)),
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
onSave={handleSaveTopics}
|
|
478
|
+
saving={savingTopics}
|
|
479
|
+
saveSuccess={topicsSaved}
|
|
480
|
+
labels={labels.topics}
|
|
481
|
+
/>
|
|
482
|
+
) : null}
|
|
483
|
+
</div>
|
|
484
|
+
) : null}
|
|
485
|
+
|
|
486
|
+
{tab === MESSAGES_TAB.TEMPLATES ? (
|
|
487
|
+
<div className="space-y-4">
|
|
488
|
+
{renderTemplatesNotice?.()}
|
|
489
|
+
<WhatsAppTemplatesSettings
|
|
490
|
+
templates={templates}
|
|
491
|
+
loadingTemplates={loadingTemplates}
|
|
492
|
+
templatesError={templatesError}
|
|
493
|
+
selectedTemplateName={templateSettings.templateName}
|
|
494
|
+
onSelectTemplate={handleSelectTemplate}
|
|
495
|
+
variables={templateSettings.variables}
|
|
496
|
+
onVariablesChange={(variables) => setTemplateSettings((previous) => ({ ...previous, variables }))}
|
|
497
|
+
onSave={handleSaveTemplate}
|
|
498
|
+
saving={savingTemplate}
|
|
499
|
+
saveSuccess={templateSaved}
|
|
500
|
+
labels={labels.templates}
|
|
501
|
+
settingsLabels={labels.templateSettings}
|
|
502
|
+
{...(api.listTemplates ? { onRefreshTemplates: () => void reloadTemplates() } : {})}
|
|
503
|
+
{...(availableVariables ? { availableVariables } : {})}
|
|
504
|
+
{...(api.createTemplate
|
|
505
|
+
? {
|
|
506
|
+
create: {
|
|
507
|
+
value: createTemplate,
|
|
508
|
+
onChange: setCreateTemplate,
|
|
509
|
+
onSubmit: handleCreateTemplate,
|
|
510
|
+
submitting: creatingTemplate,
|
|
511
|
+
result: createResult,
|
|
512
|
+
labels: labels.createTemplate,
|
|
513
|
+
...(createTemplatePreviewCompanyName ? { previewCompanyName: createTemplatePreviewCompanyName } : {}),
|
|
514
|
+
...(createTemplateVariableExamples ? { variableExamples: createTemplateVariableExamples } : {}),
|
|
515
|
+
},
|
|
516
|
+
}
|
|
517
|
+
: {})}
|
|
518
|
+
{...(templateRoles.length > 0
|
|
519
|
+
? {
|
|
520
|
+
extraRoleForms: (
|
|
521
|
+
<>
|
|
522
|
+
{templateRoles.map((role) => {
|
|
523
|
+
const settings = roleSettings[role.key] ?? EMPTY_TEMPLATE_SETTINGS
|
|
524
|
+
return (
|
|
525
|
+
<WhatsAppTemplateSettingsForm
|
|
526
|
+
key={role.key}
|
|
527
|
+
templates={templates}
|
|
528
|
+
loadingTemplates={loadingTemplates}
|
|
529
|
+
templatesError={templatesError}
|
|
530
|
+
selectedTemplateName={settings.templateName}
|
|
531
|
+
onSelectTemplate={(name, template) => handleSelectRoleTemplate(role, name, template)}
|
|
532
|
+
variables={settings.variables}
|
|
533
|
+
onVariablesChange={(variables) =>
|
|
534
|
+
setRoleSettings((previous) => ({
|
|
535
|
+
...previous,
|
|
536
|
+
[role.key]: { ...(previous[role.key] ?? EMPTY_TEMPLATE_SETTINGS), variables },
|
|
537
|
+
}))
|
|
538
|
+
}
|
|
539
|
+
onSave={handleSaveRole(role)}
|
|
540
|
+
saving={Boolean(savingRole[role.key])}
|
|
541
|
+
saveSuccess={Boolean(roleSaved[role.key])}
|
|
542
|
+
labels={{ ...labels.templateSettings, ...role.labels }}
|
|
543
|
+
{...(api.listTemplates ? { onRefreshTemplates: () => void reloadTemplates() } : {})}
|
|
544
|
+
{...(availableVariables ? { availableVariables } : {})}
|
|
545
|
+
/>
|
|
546
|
+
)
|
|
547
|
+
})}
|
|
548
|
+
</>
|
|
549
|
+
),
|
|
550
|
+
}
|
|
551
|
+
: {})}
|
|
552
|
+
/>
|
|
553
|
+
</div>
|
|
554
|
+
) : null}
|
|
555
|
+
|
|
556
|
+
{tab === MESSAGES_TAB.TRANSCRIPTION ? (
|
|
557
|
+
<TranscriptionSettingsForm
|
|
558
|
+
enabled={transcription.enabled}
|
|
559
|
+
onEnabledChange={(enabled) => setTranscription((previous) => ({ ...previous, enabled }))}
|
|
560
|
+
mode={transcription.mode}
|
|
561
|
+
onModeChange={(mode) => setTranscription((previous) => ({ ...previous, mode }))}
|
|
562
|
+
onSave={handleSaveTranscription}
|
|
563
|
+
isAvailable={transcription.available}
|
|
564
|
+
saving={savingTranscription}
|
|
565
|
+
saveSuccess={transcriptionSaved}
|
|
566
|
+
labels={labels.transcription}
|
|
567
|
+
/>
|
|
568
|
+
) : null}
|
|
569
|
+
</div>
|
|
570
|
+
)
|
|
571
|
+
}
|
|
@@ -66,6 +66,7 @@ export function TopicsForm({
|
|
|
66
66
|
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">{topic.label}</p>
|
|
67
67
|
</div>
|
|
68
68
|
<button
|
|
69
|
+
data-cv-tooltip={topic.label} aria-label={topic.label}
|
|
69
70
|
type="button"
|
|
70
71
|
onClick={() => onToggle(topic.key)}
|
|
71
72
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
|
@@ -94,6 +95,7 @@ export function TopicsForm({
|
|
|
94
95
|
|
|
95
96
|
<div className="flex items-center gap-3">
|
|
96
97
|
<button
|
|
98
|
+
data-cv-tooltip={labels.saveButton} aria-label={labels.saveButton}
|
|
97
99
|
type="submit"
|
|
98
100
|
disabled={saving}
|
|
99
101
|
className="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
|
|
4
|
+
import { TranscriptionSettingsForm } from './TranscriptionSettingsForm'
|
|
5
|
+
import type { TranscriptionSettingsFormProps } from './TranscriptionSettingsForm'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extrai uma tag pelo atributo, em vez de casar regex com a ordem dos atributos: o React SSR emite
|
|
9
|
+
* `checked=""` antes de `value=""`, então `/value="auto"[^>]*checked/` falha num componente correto.
|
|
10
|
+
*/
|
|
11
|
+
function findTag(markup: string, tagPattern: RegExp, contains: string): string | undefined {
|
|
12
|
+
return (markup.match(tagPattern) ?? []).find((tag) => tag.includes(contains))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function render(overrides: Partial<TranscriptionSettingsFormProps> = {}): string {
|
|
16
|
+
return renderToStaticMarkup(
|
|
17
|
+
<TranscriptionSettingsForm
|
|
18
|
+
enabled={false}
|
|
19
|
+
onEnabledChange={() => {}}
|
|
20
|
+
mode="onDemand"
|
|
21
|
+
onModeChange={() => {}}
|
|
22
|
+
onSave={() => {}}
|
|
23
|
+
{...overrides}
|
|
24
|
+
/>,
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('TranscriptionSettingsForm', () => {
|
|
29
|
+
it('esconde a escolha de modo enquanto estiver desligado', () => {
|
|
30
|
+
const markup = render({ enabled: false })
|
|
31
|
+
|
|
32
|
+
expect(markup).not.toContain('Quando transcrever')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('mostra as duas opções de modo ao ligar', () => {
|
|
36
|
+
const markup = render({ enabled: true })
|
|
37
|
+
|
|
38
|
+
expect(markup).toContain('Quando transcrever')
|
|
39
|
+
expect(markup).toContain('Quando o atendente pedir')
|
|
40
|
+
expect(markup).toContain('Automaticamente')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('marca o modo atual', () => {
|
|
44
|
+
const markup = render({ enabled: true, mode: 'auto' })
|
|
45
|
+
const radios = /<input type="radio"[^>]*>/g
|
|
46
|
+
|
|
47
|
+
expect(findTag(markup, radios, 'value="auto"')).toContain('checked')
|
|
48
|
+
expect(findTag(markup, radios, 'value="onDemand"')).not.toContain('checked')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* "O ambiente consegue" é diferente de "esta empresa quer". Sem o aviso, o lojista ligaria o
|
|
53
|
+
* interruptor num ambiente sem engine, nada apareceria, e nada explicaria por quê.
|
|
54
|
+
*/
|
|
55
|
+
it('avisa e bloqueia quando a capacidade não existe no servidor', () => {
|
|
56
|
+
const markup = render({ isAvailable: false })
|
|
57
|
+
|
|
58
|
+
expect(markup).toContain('não está disponível neste ambiente')
|
|
59
|
+
expect(findTag(markup, /<input type="checkbox"[^>]*>/g, 'type="checkbox"')).toContain('disabled')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('não avisa nada quando a capacidade existe', () => {
|
|
63
|
+
const markup = render({ isAvailable: true })
|
|
64
|
+
|
|
65
|
+
expect(markup).not.toContain('não está disponível neste ambiente')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('aceita rótulos do host', () => {
|
|
69
|
+
const markup = render({ labels: { sectionTitle: 'Áudio em texto' } })
|
|
70
|
+
|
|
71
|
+
expect(markup).toContain('Áudio em texto')
|
|
72
|
+
expect(markup).not.toContain('Transcrição de áudio')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('desabilita o salvar enquanto salva', () => {
|
|
76
|
+
const markup = render({ saving: true })
|
|
77
|
+
|
|
78
|
+
expect(markup).toContain('Salvando...')
|
|
79
|
+
expect(findTag(markup, /<button[^>]*>/g, 'type="submit"')).toContain('disabled')
|
|
80
|
+
})
|
|
81
|
+
})
|