@adatechnology/conversations-ui 0.1.0-rc.3 → 0.1.0-rc.31

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.
Files changed (143) hide show
  1. package/dist/chunk-CUYYYZWD.js +2317 -0
  2. package/dist/chunk-DXPSPUWF.js +110 -0
  3. package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
  4. package/dist/flows/index.d.ts +338 -4
  5. package/dist/flows/index.js +1331 -55
  6. package/dist/index.d.ts +1074 -42
  7. package/dist/index.js +3572 -742
  8. package/dist/preview/index.d.ts +328 -8
  9. package/dist/preview/index.js +902 -115
  10. package/dist/styles.css +899 -0
  11. package/dist/types-De5aN-E_.d.ts +502 -0
  12. package/package.json +3 -3
  13. package/src/AudioPlayer.tsx +8 -0
  14. package/src/AudioRecorderButton.test.tsx +30 -0
  15. package/src/AudioRecorderButton.tsx +248 -0
  16. package/src/AudioTranscription.test.tsx +115 -0
  17. package/src/AudioTranscription.tsx +252 -0
  18. package/src/Avatar.tsx +14 -3
  19. package/src/ConversationContextPanel.tsx +218 -44
  20. package/src/ConversationDocumentsPanel.tsx +347 -24
  21. package/src/ConversationHeader.test.tsx +66 -0
  22. package/src/ConversationHeader.tsx +163 -45
  23. package/src/ConversationListItem.tsx +19 -2
  24. package/src/ConversationLocalesProvider.tsx +42 -0
  25. package/src/ConversationRow.tsx +31 -7
  26. package/src/DocumentsLibrary.tsx +382 -0
  27. package/src/EmojiPicker.tsx +70 -55
  28. package/src/FileIcon.test.ts +83 -0
  29. package/src/FileIcon.tsx +88 -11
  30. package/src/InteractiveMessage.test.tsx +41 -0
  31. package/src/InteractiveMessage.tsx +146 -0
  32. package/src/Lightbox.tsx +18 -3
  33. package/src/MediaRenderer.tsx +98 -22
  34. package/src/MessageBubble.test.tsx +41 -0
  35. package/src/MessageBubble.tsx +75 -6
  36. package/src/MessageComposer.test.tsx +35 -0
  37. package/src/MessageComposer.tsx +155 -19
  38. package/src/RichMessageComposer.test.tsx +113 -0
  39. package/src/RichMessageComposer.tsx +551 -0
  40. package/src/SimpleEmojiPicker.tsx +5 -3
  41. package/src/StatusTicks.tsx +1 -1
  42. package/src/Toast.tsx +4 -0
  43. package/src/Tooltip.test.ts +42 -0
  44. package/src/Tooltip.tsx +164 -0
  45. package/src/Wallpaper.test.tsx +21 -0
  46. package/src/Wallpaper.tsx +67 -7
  47. package/src/WhatsAppMessageEditor.tsx +28 -4
  48. package/src/WindowExpiredNotice.tsx +12 -4
  49. package/src/audioRecorderFormat.test.ts +67 -0
  50. package/src/buildOutput.test.ts +79 -0
  51. package/src/composer.constant.ts +33 -0
  52. package/src/conversationTranscript.test.ts +57 -0
  53. package/src/conversationTranscript.ts +29 -4
  54. package/src/conversationWindow.ts +7 -5
  55. package/src/documentTypeLabel.test.ts +57 -0
  56. package/src/documents/DocumentsWorkspace.tsx +550 -0
  57. package/src/documents/index.ts +8 -0
  58. package/src/documents/labels.ts +92 -0
  59. package/src/emojiCatalog.test.ts +35 -0
  60. package/src/emojiCatalog.ts +189 -0
  61. package/src/flows/FlowGroupHeader.tsx +12 -2
  62. package/src/flows/FlowMapCanvas.tsx +15 -12
  63. package/src/flows/FlowMapNode.tsx +4 -1
  64. package/src/flows/FlowNodeCard.tsx +35 -8
  65. package/src/flows/FlowNodePanel.tsx +149 -38
  66. package/src/flows/FlowPalette.tsx +13 -3
  67. package/src/flows/FlowPortalNode.tsx +1 -1
  68. package/src/flows/FlowWhatsAppPreview.tsx +14 -3
  69. package/src/flows/FlowsWorkspace.tsx +1023 -0
  70. package/src/flows/flowCanvasModel.test.ts +312 -0
  71. package/src/flows/flowCanvasModel.ts +368 -0
  72. package/src/flows/flowEditorOps.test.ts +241 -0
  73. package/src/flows/flowEditorOps.ts +177 -0
  74. package/src/flows/flowGraph.ts +6 -6
  75. package/src/flows/index.ts +47 -1
  76. package/src/flows/labels.ts +141 -0
  77. package/src/flows/workspaceContract.test.ts +95 -0
  78. package/src/hooks/useContainerWidth.ts +35 -0
  79. package/src/hooks/useConversationActions.ts +56 -0
  80. package/src/hooks/useConversationDocuments.ts +11 -7
  81. package/src/hooks/useConversationList.ts +15 -9
  82. package/src/hooks/useConversationMessages.ts +2 -2
  83. package/src/hooks/useConversationRealtime.ts +10 -8
  84. package/src/hooks/useScrollToLatestMessage.ts +127 -0
  85. package/src/hooks/useUrlFilterState.ts +107 -0
  86. package/src/icon.constant.ts +12 -0
  87. package/src/index.ts +114 -13
  88. package/src/lib/cn.test.ts +29 -0
  89. package/src/lib/composer-formatting.test.ts +78 -0
  90. package/src/lib/composer-formatting.ts +145 -0
  91. package/src/lib/createMediaUrlResolver.ts +33 -0
  92. package/src/lib/paginated.test.ts +33 -0
  93. package/src/lib/paginated.ts +26 -0
  94. package/src/lib/whatsapp-formatting.test.tsx +37 -0
  95. package/src/lib/whatsapp-formatting.tsx +28 -3
  96. package/src/listing/index.tsx +202 -0
  97. package/src/pagination.constant.ts +10 -0
  98. package/src/preview/ConversationPreview.tsx +199 -13
  99. package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
  100. package/src/preview/ConversationSimulatorPanel.tsx +89 -0
  101. package/src/preview/MediaTypesPreview.tsx +87 -0
  102. package/src/preview/conversationPreviewFailures.test.ts +64 -0
  103. package/src/preview/createMockConversationsApi.ts +175 -15
  104. package/src/preview/createPreviewBridgeClient.test.ts +92 -0
  105. package/src/preview/createPreviewBridgeClient.ts +124 -0
  106. package/src/preview/createPreviewMediaUploader.ts +82 -0
  107. package/src/preview/createPreviewWebhookClient.test.ts +96 -0
  108. package/src/preview/createPreviewWebhookClient.ts +127 -4
  109. package/src/preview/index.ts +33 -3
  110. package/src/preview/mediaTypeOf.test.ts +15 -0
  111. package/src/preview/mockDocumentsSearch.test.ts +57 -0
  112. package/src/preview/preview.test.ts +5 -3
  113. package/src/preview/previewFileSamples.test.ts +151 -0
  114. package/src/preview/previewFileSamples.ts +74 -0
  115. package/src/preview/previewFixtures.ts +288 -1
  116. package/src/preview/previewMediaSource.test.ts +62 -0
  117. package/src/preview/previewMediaSource.ts +91 -0
  118. package/src/preview/previewMediaUploader.test.ts +61 -0
  119. package/src/providers/ConversationsProvider.tsx +8 -6
  120. package/src/providers/types.ts +185 -10
  121. package/src/quickReply.test.ts +58 -0
  122. package/src/settings/MessagesWorkspace.tsx +468 -0
  123. package/src/settings/TopicsForm.tsx +2 -0
  124. package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
  125. package/src/settings/TranscriptionSettingsForm.tsx +190 -0
  126. package/src/settings/WelcomeFarewellForm.tsx +1 -0
  127. package/src/settings/WhatsAppCreateTemplateForm.tsx +4 -1
  128. package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
  129. package/src/settings/WhatsAppTemplatesSettings.tsx +9 -1
  130. package/src/styles.css +862 -0
  131. package/src/types.ts +64 -1
  132. package/src/useWaitingNotifications.ts +74 -29
  133. package/src/workspace/BulkTemplateModal.tsx +132 -0
  134. package/src/workspace/ConversationPane.tsx +432 -0
  135. package/src/workspace/ConversationsInboxList.tsx +194 -0
  136. package/src/workspace/ConversationsWorkspace.tsx +346 -0
  137. package/src/workspace/index.ts +12 -0
  138. package/src/workspace/labels.test.ts +17 -0
  139. package/src/workspace/labels.ts +85 -0
  140. package/src/workspace/useConversationsInbox.ts +332 -0
  141. package/dist/chunk-N7B24WYD.js +0 -719
  142. package/dist/chunk-NV2RZ5KT.js +0 -56
  143. package/dist/types-C0PtaO7S.d.ts +0 -207
@@ -0,0 +1,468 @@
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 type {
22
+ WhatsAppTemplateSummary,
23
+ WhatsAppTemplateVariableSuggestion,
24
+ WhatsAppTemplateSettingsFormLabels,
25
+ } from './WhatsAppTemplateSettingsForm'
26
+ import type {
27
+ WhatsAppCreateTemplateFormLabels,
28
+ WhatsAppCreateTemplateResult,
29
+ WhatsAppCreateTemplateState,
30
+ } from './WhatsAppCreateTemplateForm'
31
+ import {
32
+ TranscriptionSettingsForm,
33
+ type TranscriptionSettingsFormLabels,
34
+ } from './TranscriptionSettingsForm'
35
+ import type { TranscriptionMode } from '../types'
36
+ import { TooltipLayer } from '../Tooltip'
37
+
38
+ export interface BotMessages {
39
+ welcomeMessage: string
40
+ farewellMessage: string
41
+ }
42
+
43
+ export interface TemplateSettings {
44
+ templateName: string
45
+ templateLanguage: string
46
+ variables: string[]
47
+ }
48
+
49
+ export interface TranscriptionSettings {
50
+ enabled: boolean
51
+ mode: TranscriptionMode
52
+ available: boolean
53
+ }
54
+
55
+ export interface MessagesWorkspaceApi {
56
+ getMessages(): Promise<BotMessages>
57
+ saveMessages(messages: BotMessages): Promise<void>
58
+ /** Sem este par, a seção de tópicos não é desenhada. */
59
+ getTopics?(): Promise<TopicItem[]>
60
+ saveTopics?(topics: TopicItem[]): Promise<void>
61
+ /** Sem este par, a aba de templates não é desenhada. */
62
+ getTemplateSettings?(): Promise<TemplateSettings>
63
+ saveTemplateSettings?(settings: TemplateSettings): Promise<void>
64
+ /**
65
+ * Lista de templates aprovados na Meta. Ausente, a aba ainda existe (dá para salvar o nome
66
+ * escolhido), mas nasce sem opções e sem botão de recarregar.
67
+ */
68
+ listTemplates?(): Promise<WhatsAppTemplateSummary[]>
69
+ /** Sem isto, o submenu "Criar template" some. */
70
+ createTemplate?(input: WhatsAppCreateTemplateState): Promise<WhatsAppCreateTemplateResult>
71
+ /** Sem este par, a aba de transcrição não é desenhada. */
72
+ getTranscription?(): Promise<TranscriptionSettings>
73
+ saveTranscription?(settings: Omit<TranscriptionSettings, 'available'>): Promise<void>
74
+ }
75
+
76
+ export interface MessagesWorkspaceLabels {
77
+ title: string
78
+ subtitle: string
79
+ loading: string
80
+ loadError: string
81
+ tabBot: string
82
+ tabTemplates: string
83
+ tabTranscription: string
84
+ welcomeFarewell: Partial<WelcomeFarewellFormLabels>
85
+ topics: Partial<TopicsFormLabels>
86
+ templates: Partial<WhatsAppTemplatesSettingsLabels>
87
+ templateSettings: Partial<WhatsAppTemplateSettingsFormLabels>
88
+ createTemplate: Partial<WhatsAppCreateTemplateFormLabels>
89
+ transcription: Partial<TranscriptionSettingsFormLabels>
90
+ }
91
+
92
+ const DEFAULT_LABELS: MessagesWorkspaceLabels = {
93
+ title: 'Mensagens',
94
+ subtitle: 'Mensagens do bot e templates do WhatsApp.',
95
+ loading: 'Carregando configurações…',
96
+ loadError: 'Não foi possível carregar as configurações de mensagens.',
97
+ tabBot: 'Mensagens do Bot',
98
+ tabTemplates: 'Templates WhatsApp',
99
+ tabTranscription: 'Transcrição de áudio',
100
+ welcomeFarewell: {},
101
+ topics: {},
102
+ templates: {},
103
+ templateSettings: {},
104
+ createTemplate: {},
105
+ transcription: {},
106
+ }
107
+
108
+ const MESSAGES_TAB = {
109
+ BOT: 'bot',
110
+ TEMPLATES: 'templates',
111
+ TRANSCRIPTION: 'transcription',
112
+ } as const
113
+ type MessagesTab = (typeof MESSAGES_TAB)[keyof typeof MESSAGES_TAB]
114
+
115
+ const EMPTY_MESSAGES: BotMessages = { welcomeMessage: '', farewellMessage: '' }
116
+ const EMPTY_TEMPLATE_SETTINGS: TemplateSettings = {
117
+ templateName: '',
118
+ templateLanguage: 'pt_BR',
119
+ variables: [],
120
+ }
121
+ const EMPTY_CREATE_TEMPLATE: WhatsAppCreateTemplateState = {
122
+ name: '',
123
+ category: 'UTILITY',
124
+ language: 'pt_BR',
125
+ headerType: 'NONE',
126
+ headerText: '',
127
+ bodyText: '',
128
+ footerText: '',
129
+ }
130
+
131
+ /**
132
+ * Padrão da instalação quando ninguém decidiu ainda.
133
+ *
134
+ * O servidor guarda `null` de propósito (herda o ambiente), mas o interruptor é booleano —
135
+ * `false` é a leitura honesta de "ninguém ligou aqui"; marcado sugeriria escolha que não houve.
136
+ */
137
+ const UNDECIDED_TRANSCRIPTION: TranscriptionSettings = {
138
+ enabled: false,
139
+ mode: 'onDemand',
140
+ available: false,
141
+ }
142
+
143
+ const SAVE_FEEDBACK_MS = 3000
144
+
145
+ export interface MessagesWorkspaceProps {
146
+ readonly api: MessagesWorkspaceApi
147
+ readonly labels?: Partial<MessagesWorkspaceLabels>
148
+ readonly welcomePlaceholders?: readonly string[]
149
+ readonly farewellPlaceholders?: readonly string[]
150
+ readonly availableVariables?: WhatsAppTemplateVariableSuggestion[]
151
+ /** Aviso do produto acima da aba de templates (ex.: rota da Graph API ainda não implementada). */
152
+ readonly renderTemplatesNotice?: () => ReactNode
153
+ readonly className?: string
154
+ }
155
+
156
+ export function MessagesWorkspace({
157
+ api,
158
+ labels: labelsOverride,
159
+ welcomePlaceholders,
160
+ farewellPlaceholders,
161
+ availableVariables,
162
+ renderTemplatesNotice,
163
+ className,
164
+ }: MessagesWorkspaceProps) {
165
+ const labels = { ...DEFAULT_LABELS, ...labelsOverride }
166
+
167
+ const hasTopics = Boolean(api.getTopics && api.saveTopics)
168
+ const hasTemplates = Boolean(api.getTemplateSettings && api.saveTemplateSettings)
169
+ const hasTranscription = Boolean(api.getTranscription && api.saveTranscription)
170
+
171
+ const [tab, setTab] = useState<MessagesTab>(MESSAGES_TAB.BOT)
172
+ const [loadState, setLoadState] = useState<'loading' | 'ready' | 'error'>('loading')
173
+
174
+ const [messages, setMessages] = useState<BotMessages>(EMPTY_MESSAGES)
175
+ const [savingMessages, setSavingMessages] = useState(false)
176
+ const [messagesSaved, setMessagesSaved] = useState(false)
177
+
178
+ const [topics, setTopics] = useState<TopicItem[]>([])
179
+ const [savingTopics, setSavingTopics] = useState(false)
180
+ const [topicsSaved, setTopicsSaved] = useState(false)
181
+
182
+ const [templateSettings, setTemplateSettings] = useState<TemplateSettings>(EMPTY_TEMPLATE_SETTINGS)
183
+ const [templates, setTemplates] = useState<WhatsAppTemplateSummary[]>([])
184
+ const [loadingTemplates, setLoadingTemplates] = useState(false)
185
+ const [templatesError, setTemplatesError] = useState(false)
186
+ const [savingTemplate, setSavingTemplate] = useState(false)
187
+ const [templateSaved, setTemplateSaved] = useState(false)
188
+
189
+ const [createTemplate, setCreateTemplate] = useState<WhatsAppCreateTemplateState>(EMPTY_CREATE_TEMPLATE)
190
+ const [creatingTemplate, setCreatingTemplate] = useState(false)
191
+ const [createResult, setCreateResult] = useState<WhatsAppCreateTemplateResult | null>(null)
192
+
193
+ const [transcription, setTranscription] = useState<TranscriptionSettings>(UNDECIDED_TRANSCRIPTION)
194
+ const [savingTranscription, setSavingTranscription] = useState(false)
195
+ const [transcriptionSaved, setTranscriptionSaved] = useState(false)
196
+
197
+ const reloadTemplates = useCallback(async () => {
198
+ if (!api.listTemplates) return
199
+ setLoadingTemplates(true)
200
+ setTemplatesError(false)
201
+ try {
202
+ setTemplates(await api.listTemplates())
203
+ } catch {
204
+ setTemplatesError(true)
205
+ } finally {
206
+ setLoadingTemplates(false)
207
+ }
208
+ }, [api])
209
+
210
+ useEffect(() => {
211
+ let active = true
212
+ async function load(): Promise<void> {
213
+ try {
214
+ const [loadedMessages, loadedTopics, loadedTemplateSettings, loadedTranscription] = await Promise.all([
215
+ api.getMessages(),
216
+ api.getTopics?.(),
217
+ api.getTemplateSettings?.(),
218
+ api.getTranscription?.(),
219
+ ])
220
+ if (!active) return
221
+ setMessages(loadedMessages)
222
+ if (loadedTopics) setTopics(loadedTopics)
223
+ if (loadedTemplateSettings) setTemplateSettings(loadedTemplateSettings)
224
+ if (loadedTranscription) setTranscription(loadedTranscription)
225
+ setLoadState('ready')
226
+ } catch {
227
+ if (active) setLoadState('error')
228
+ }
229
+ }
230
+ void load()
231
+ return () => {
232
+ active = false
233
+ }
234
+ }, [api])
235
+
236
+ // A lista de templates é buscada à parte porque falha sozinha: a Graph API cai, ou a rota nem
237
+ // existe no produto, e nada disso pode impedir a tela de abrir para salvar o resto.
238
+ useEffect(() => {
239
+ void reloadTemplates()
240
+ }, [reloadTemplates])
241
+
242
+ function flash(setter: (value: boolean) => void): void {
243
+ setter(true)
244
+ setTimeout(() => setter(false), SAVE_FEEDBACK_MS)
245
+ }
246
+
247
+ async function handleSaveMessages(event: FormEvent): Promise<void> {
248
+ event.preventDefault()
249
+ setSavingMessages(true)
250
+ try {
251
+ await api.saveMessages(messages)
252
+ flash(setMessagesSaved)
253
+ } finally {
254
+ setSavingMessages(false)
255
+ }
256
+ }
257
+
258
+ async function handleSaveTopics(event: FormEvent): Promise<void> {
259
+ event.preventDefault()
260
+ if (!api.saveTopics) return
261
+ setSavingTopics(true)
262
+ try {
263
+ await api.saveTopics(topics)
264
+ flash(setTopicsSaved)
265
+ } finally {
266
+ setSavingTopics(false)
267
+ }
268
+ }
269
+
270
+ async function handleSaveTemplate(event: FormEvent): Promise<void> {
271
+ event.preventDefault()
272
+ if (!api.saveTemplateSettings) return
273
+ setSavingTemplate(true)
274
+ try {
275
+ await api.saveTemplateSettings(templateSettings)
276
+ flash(setTemplateSaved)
277
+ } finally {
278
+ setSavingTemplate(false)
279
+ }
280
+ }
281
+
282
+ async function handleCreateTemplate(event: FormEvent): Promise<void> {
283
+ event.preventDefault()
284
+ if (!api.createTemplate) return
285
+ setCreatingTemplate(true)
286
+ setCreateResult(null)
287
+ try {
288
+ const result = await api.createTemplate(createTemplate)
289
+ setCreateResult(result)
290
+ if (result.ok) {
291
+ setCreateTemplate(EMPTY_CREATE_TEMPLATE)
292
+ void reloadTemplates()
293
+ }
294
+ } catch (error) {
295
+ setCreateResult({ ok: false, message: error instanceof Error ? error.message : '' })
296
+ } finally {
297
+ setCreatingTemplate(false)
298
+ }
299
+ }
300
+
301
+ async function handleSaveTranscription(event: FormEvent): Promise<void> {
302
+ event.preventDefault()
303
+ if (!api.saveTranscription) return
304
+ setSavingTranscription(true)
305
+ try {
306
+ await api.saveTranscription({ enabled: transcription.enabled, mode: transcription.mode })
307
+ flash(setTranscriptionSaved)
308
+ } finally {
309
+ setSavingTranscription(false)
310
+ }
311
+ }
312
+
313
+ function handleSelectTemplate(name: string, template: WhatsAppTemplateSummary | undefined): void {
314
+ setTemplateSettings((previous) => {
315
+ if (!template) return { ...previous, templateName: name }
316
+ // Escolher um template com variáveis e deixar a lista vazia é o erro clássico: o envio
317
+ // vai à Meta sem os parâmetros e volta rejeitado. Pré-abrir os campos evita isso — mas só
318
+ // quando ninguém preencheu nada ainda, para não apagar mapeamento existente.
319
+ const shouldSeedVariables = template.variableCount > 0 && previous.variables.length === 0
320
+ return {
321
+ templateName: name,
322
+ templateLanguage: template.language,
323
+ variables: shouldSeedVariables
324
+ ? Array.from({ length: template.variableCount }, () => '')
325
+ : previous.variables,
326
+ }
327
+ })
328
+ }
329
+
330
+ if (loadState === 'loading') {
331
+ return <p className="p-6 text-sm text-gray-500 dark:text-gray-400">{labels.loading}</p>
332
+ }
333
+ if (loadState === 'error') {
334
+ return (
335
+ <p role="alert" className="p-6 text-sm text-red-600 dark:text-red-400">
336
+ {labels.loadError}
337
+ </p>
338
+ )
339
+ }
340
+
341
+ const tabs: { id: MessagesTab; label: string }[] = [
342
+ { id: MESSAGES_TAB.BOT, label: labels.tabBot },
343
+ ...(hasTemplates ? [{ id: MESSAGES_TAB.TEMPLATES, label: labels.tabTemplates }] : []),
344
+ ...(hasTranscription ? [{ id: MESSAGES_TAB.TRANSCRIPTION, label: labels.tabTranscription }] : []),
345
+ ]
346
+
347
+ return (
348
+ <div className={`space-y-6 p-4 lg:p-0 ${className ?? ''}`}>
349
+ <TooltipLayer />
350
+ <header>
351
+ <h1 className="text-xl font-semibold text-gray-900 dark:text-gray-100">{labels.title}</h1>
352
+ <p className="text-sm text-gray-500 dark:text-gray-400">{labels.subtitle}</p>
353
+ </header>
354
+
355
+ {/* Uma aba só não é navegação: vira ruído com aparência de escolha. */}
356
+ {tabs.length > 1 ? (
357
+ <nav className="flex gap-1 border-b border-gray-200 dark:border-gray-700" role="tablist">
358
+ {tabs.map((item) => (
359
+ <button
360
+ data-cv-tooltip={item.label} aria-label={item.label}
361
+ key={item.id}
362
+ type="button"
363
+ role="tab"
364
+ aria-selected={tab === item.id}
365
+ onClick={() => setTab(item.id)}
366
+ className={`-mb-px border-b-2 px-4 py-2.5 text-sm font-medium transition-colors ${
367
+ tab === item.id
368
+ ? 'border-blue-600 text-blue-700 dark:border-blue-400 dark:text-blue-400'
369
+ : 'border-transparent text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200'
370
+ }`}
371
+ >
372
+ {item.label}
373
+ </button>
374
+ ))}
375
+ </nav>
376
+ ) : null}
377
+
378
+ {tab === MESSAGES_TAB.BOT ? (
379
+ <div className="space-y-8">
380
+ <WelcomeFarewellForm
381
+ welcomeMessage={messages.welcomeMessage}
382
+ onWelcomeMessageChange={(welcomeMessage) =>
383
+ setMessages((previous) => ({ ...previous, welcomeMessage }))
384
+ }
385
+ farewellMessage={messages.farewellMessage}
386
+ onFarewellMessageChange={(farewellMessage) =>
387
+ setMessages((previous) => ({ ...previous, farewellMessage }))
388
+ }
389
+ onSave={handleSaveMessages}
390
+ saving={savingMessages}
391
+ saveSuccess={messagesSaved}
392
+ labels={labels.welcomeFarewell}
393
+ {...(welcomePlaceholders ? { welcomePlaceholders } : {})}
394
+ {...(farewellPlaceholders ? { farewellPlaceholders } : {})}
395
+ />
396
+
397
+ {hasTopics ? (
398
+ <TopicsForm
399
+ topics={topics}
400
+ onToggle={(key) =>
401
+ setTopics((previous) =>
402
+ previous.map((topic) => (topic.key === key ? { ...topic, enabled: !topic.enabled } : topic)),
403
+ )
404
+ }
405
+ onMessageChange={(key, message) =>
406
+ setTopics((previous) =>
407
+ previous.map((topic) => (topic.key === key ? { ...topic, message } : topic)),
408
+ )
409
+ }
410
+ onSave={handleSaveTopics}
411
+ saving={savingTopics}
412
+ saveSuccess={topicsSaved}
413
+ labels={labels.topics}
414
+ />
415
+ ) : null}
416
+ </div>
417
+ ) : null}
418
+
419
+ {tab === MESSAGES_TAB.TEMPLATES ? (
420
+ <div className="space-y-4">
421
+ {renderTemplatesNotice?.()}
422
+ <WhatsAppTemplatesSettings
423
+ templates={templates}
424
+ loadingTemplates={loadingTemplates}
425
+ templatesError={templatesError}
426
+ selectedTemplateName={templateSettings.templateName}
427
+ onSelectTemplate={handleSelectTemplate}
428
+ variables={templateSettings.variables}
429
+ onVariablesChange={(variables) => setTemplateSettings((previous) => ({ ...previous, variables }))}
430
+ onSave={handleSaveTemplate}
431
+ saving={savingTemplate}
432
+ saveSuccess={templateSaved}
433
+ labels={labels.templates}
434
+ settingsLabels={labels.templateSettings}
435
+ {...(api.listTemplates ? { onRefreshTemplates: () => void reloadTemplates() } : {})}
436
+ {...(availableVariables ? { availableVariables } : {})}
437
+ {...(api.createTemplate
438
+ ? {
439
+ create: {
440
+ value: createTemplate,
441
+ onChange: setCreateTemplate,
442
+ onSubmit: handleCreateTemplate,
443
+ submitting: creatingTemplate,
444
+ result: createResult,
445
+ labels: labels.createTemplate,
446
+ },
447
+ }
448
+ : {})}
449
+ />
450
+ </div>
451
+ ) : null}
452
+
453
+ {tab === MESSAGES_TAB.TRANSCRIPTION ? (
454
+ <TranscriptionSettingsForm
455
+ enabled={transcription.enabled}
456
+ onEnabledChange={(enabled) => setTranscription((previous) => ({ ...previous, enabled }))}
457
+ mode={transcription.mode}
458
+ onModeChange={(mode) => setTranscription((previous) => ({ ...previous, mode }))}
459
+ onSave={handleSaveTranscription}
460
+ isAvailable={transcription.available}
461
+ saving={savingTranscription}
462
+ saveSuccess={transcriptionSaved}
463
+ labels={labels.transcription}
464
+ />
465
+ ) : null}
466
+ </div>
467
+ )
468
+ }
@@ -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
+ })