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

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-DXPSPUWF.js +110 -0
  2. package/dist/chunk-GY472G6E.js +2316 -0
  3. package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
  4. package/dist/flows/index.d.ts +311 -4
  5. package/dist/flows/index.js +1322 -55
  6. package/dist/index.d.ts +1074 -42
  7. package/dist/index.js +3571 -742
  8. package/dist/preview/index.d.ts +328 -8
  9. package/dist/preview/index.js +902 -115
  10. package/dist/styles.css +819 -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 +543 -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 +1003 -0
  70. package/src/flows/flowCanvasModel.test.ts +293 -0
  71. package/src/flows/flowCanvasModel.ts +342 -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 +40 -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 +783 -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
@@ -1,31 +1,51 @@
1
1
  /**
2
2
  * "Suas Seleções": o que o bot já coletou na conversa. Para o atendente que assume no meio, é a
3
- * diferença entre ler o transcript inteiro e ver o estado em duas linhas.
3
+ * diferença entre ler o transcript inteiro e ver o estado de relance.
4
4
  *
5
5
  * O pacote não interpreta o contexto — ele é `Record<string, unknown>` e cada produto nomeia as
6
6
  * próprias chaves. O host traduz para `entries`; aqui só se decide como mostrar.
7
+ *
8
+ * Desenho em paridade com financiamento-imobiliario-bot/apps/web/src/components/SelectionsSummary.tsx:
9
+ * card com gradiente, badge de contagem, pills de status no cabeçalho e grid de cards com borda
10
+ * colorida por estado. O que ficou diferente de lá, e por quê, está comentado no ponto.
7
11
  */
8
12
 
9
13
  import { useState } from 'react'
14
+ import { Check, ChevronDown, ChevronUp, Clock, Pencil } from 'lucide-react'
10
15
 
11
16
  import { cn } from './lib/cn'
12
17
  import { useIsNarrow } from './useIsNarrow'
13
18
 
19
+ /**
20
+ * `completed` tem valor, `pending` ainda não foi coletado, `editing` é o cliente refazendo a
21
+ * resposta. O host só precisa mandar `status` para o terceiro caso — os dois primeiros saem do
22
+ * próprio `value`, e exigir o campo quebraria quem já usa o painel.
23
+ */
24
+ export type ConversationContextStatus = 'completed' | 'pending' | 'editing'
25
+
14
26
  export interface ConversationContextEntry {
15
27
  key: string
16
28
  label: string
17
29
  value?: string | undefined
18
30
  icon?: string
31
+ status?: ConversationContextStatus
19
32
  }
20
33
 
21
34
  export interface ConversationContextPanelLabels {
22
35
  title: string
23
36
  empty: string
37
+ collapse: string
38
+ expand: string
39
+ /** Placeholder do card ainda não coletado. */
40
+ notCollected: string
24
41
  }
25
42
 
26
43
  export const DEFAULT_CONVERSATION_CONTEXT_LABELS: ConversationContextPanelLabels = {
27
- title: '📋 Suas Seleções',
44
+ title: 'Suas Seleções',
28
45
  empty: 'Nada coletado ainda nesta conversa.',
46
+ collapse: 'fechar',
47
+ expand: 'abrir',
48
+ notCollected: 'a coletar',
29
49
  }
30
50
 
31
51
  export interface ConversationContextPanelClassNames {
@@ -37,70 +57,224 @@ export interface ConversationContextPanelClassNames {
37
57
 
38
58
  export interface ConversationContextPanelProps {
39
59
  entries: readonly ConversationContextEntry[]
60
+ /**
61
+ * Estado inicial. Ausente, abre sozinho no desktop quando há algum dado preenchido.
62
+ *
63
+ * Existe porque "abre sozinho" nem sempre é o que o produto quer: com 1 de 6 campos preenchidos o
64
+ * painel ocupa altura mostrando quase só pendências, e empurra a conversa — que é o que se veio ver.
65
+ */
66
+ defaultOpen?: boolean
67
+ /**
68
+ * Rótulo do fluxo em curso, exibido em azul ao lado do título — no financiamento é o produto
69
+ * escolhido ("Consórcio", "MCMV"). Ausente, o cabeçalho fica só com título e contagens.
70
+ */
71
+ flowLabel?: string
40
72
  labels?: Partial<ConversationContextPanelLabels>
41
73
  className?: string
42
74
  classNames?: Partial<ConversationContextPanelClassNames>
43
75
  }
44
76
 
77
+ function statusOf(entry: ConversationContextEntry): ConversationContextStatus {
78
+ if (entry.status) return entry.status
79
+ return entry.value ? 'completed' : 'pending'
80
+ }
81
+
82
+ /** Borda e fundo do card por estado. Pendente é tracejado — ver o comentário no grid. */
83
+ const CARD_STYLE: Record<ConversationContextStatus, string> = {
84
+ completed: 'bg-white dark:bg-gray-800 border-2 border-green-200 dark:border-green-800 shadow-sm',
85
+ editing: 'bg-white dark:bg-gray-800 border-2 border-blue-200 dark:border-blue-800 shadow-sm',
86
+ pending: 'border-2 border-dashed border-gray-200 bg-white/40 dark:border-gray-700 dark:bg-gray-800/30',
87
+ }
88
+
89
+ const GLYPH_STYLE: Record<ConversationContextStatus, string> = {
90
+ completed: 'text-green-600 dark:text-green-400',
91
+ editing: 'text-blue-600 dark:text-blue-400',
92
+ pending: 'text-gray-400 dark:text-gray-500',
93
+ }
94
+
95
+ function StatusGlyph({ status }: { status: ConversationContextStatus }) {
96
+ const size = 11
97
+ if (status === 'completed') return <Check size={size} aria-hidden />
98
+ if (status === 'editing') return <Pencil size={size} aria-hidden />
99
+ return <Clock size={size} aria-hidden />
100
+ }
101
+
102
+ function CountPill({
103
+ status,
104
+ count,
105
+ className,
106
+ }: {
107
+ status: ConversationContextStatus
108
+ count: number
109
+ className: string
110
+ }) {
111
+ if (count === 0) return null
112
+
113
+ return (
114
+ <span className={cn('flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium', className)}>
115
+ <StatusGlyph status={status} />
116
+ {count}
117
+ </span>
118
+ )
119
+ }
120
+
45
121
  export function ConversationContextPanel({
46
122
  entries,
123
+ defaultOpen,
124
+ flowLabel,
47
125
  labels: labelsOverride,
48
126
  className,
49
127
  classNames,
50
128
  }: ConversationContextPanelProps) {
51
129
  const labels = { ...DEFAULT_CONVERSATION_CONTEXT_LABELS, ...labelsOverride }
52
- const filled = entries.filter((entry) => Boolean(entry.value))
53
130
 
54
- // Abre sozinho só quando há algo coletado: um painel de seis traços ocupa a altura da conversa
55
- // sem informar nada. O contador continua visível fechado, que é o dado útil de relance.
131
+ const counts = { completed: 0, pending: 0, editing: 0 }
132
+ for (const entry of entries) counts[statusOf(entry)] += 1
133
+
134
+ // Abre sozinho só quando há algo coletado: um painel de seis pendências ocupa a altura da conversa
135
+ // sem informar nada. As contagens continuam visíveis fechado, que é o dado útil de relance.
56
136
  //
57
137
  // `undefined` = usuário ainda não mexeu. Guardar só um booleano não serve: o contexto chega
58
138
  // depois da montagem, então o estado inicial seria calculado com a lista vazia e o painel ficaria
59
139
  // fechado mesmo com dados.
60
140
  const isNarrow = useIsNarrow()
61
141
  const [manualOpen, setManualOpen] = useState<boolean | undefined>(undefined)
62
- // No celular nasce fechado mesmo com dados: aberto, o painel consome ~150px da conversa. O
63
- // contador no cabeçalho já entrega a informação de relance.
64
- const open = manualOpen ?? (!isNarrow && filled.length > 0)
142
+ // No celular nasce fechado mesmo com dados: aberto, o painel consome ~150px da conversa.
143
+ const open = manualOpen ?? defaultOpen ?? (!isNarrow && counts.completed > 0)
65
144
 
66
145
  return (
67
- <section className={cn('border-b', classNames?.root, className)}>
68
- <button
69
- type="button"
70
- onClick={() => setManualOpen(!open)}
71
- aria-expanded={open}
72
- className={cn('flex w-full items-center gap-2 px-4 py-3 text-left text-sm font-medium', classNames?.toggle)}
146
+ // Faixa azul clara com separador, como o wrapper do financiamento: sem ela o card encostava
147
+ // direto no papel de parede da conversa e os dois blocos se misturavam.
148
+ <div className={cn('border-b bg-blue-50/60 px-3 py-3 dark:border-gray-700 dark:bg-blue-950/20', className)}>
149
+ <section
150
+ className={cn(
151
+ 'overflow-hidden rounded-lg border border-blue-200 bg-gradient-to-br from-blue-50 to-indigo-50 dark:border-blue-900 dark:from-blue-950/30 dark:to-indigo-950/30',
152
+ classNames?.root,
153
+ )}
73
154
  >
74
- <span aria-hidden className="text-xs">
75
- {open ? '▾' : '▸'}
76
- </span>
77
- <span>{labels.title}</span>
78
- <span className={cn('rounded-full bg-gray-200 px-2 text-xs dark:bg-gray-700', classNames?.counter)}>
79
- {filled.length}/{entries.length}
80
- </span>
81
- </button>
82
-
83
- {open ? (
84
- <div className={cn('px-4 pb-3', classNames?.body)}>
85
- {entries.length === 0 ? (
86
- <p className="text-xs text-gray-500">{labels.empty}</p>
87
- ) : (
88
- <dl className="grid grid-cols-2 gap-2 text-xs">
89
- {entries.map((entry) => (
90
- <div key={entry.key} className="min-w-0">
91
- <dt className="truncate text-gray-500">
92
- {entry.icon ? `${entry.icon} ` : ''}
93
- {entry.label}
94
- </dt>
95
- <dd className="truncate font-medium">
96
- {entry.value ? <span className="text-green-700 dark:text-green-400">✓ {entry.value}</span> : '—'}
97
- </dd>
98
- </div>
99
- ))}
100
- </dl>
155
+ <button
156
+ type="button"
157
+ onClick={() => setManualOpen(!open)}
158
+ aria-expanded={open}
159
+ data-cv-tooltip={open ? labels.collapse : labels.expand} aria-label={open ? labels.collapse : labels.expand}
160
+ className={cn(
161
+ 'flex w-full cursor-pointer items-center justify-between gap-2 px-4 py-2.5 text-left transition-colors hover:bg-blue-100/40 dark:hover:bg-blue-900/20',
162
+ classNames?.toggle,
101
163
  )}
102
- </div>
103
- ) : null}
104
- </section>
164
+ >
165
+ <span className="flex min-w-0 items-center gap-2">
166
+ <span aria-hidden className="text-base leading-none">
167
+ 📋
168
+ </span>
169
+ <span className="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">{labels.title}</span>
170
+ {flowLabel ? (
171
+ <span className="truncate text-xs font-medium text-blue-600 dark:text-blue-400">{flowLabel}</span>
172
+ ) : null}
173
+ <span
174
+ className={cn(
175
+ 'inline-flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-blue-600 text-xs font-medium text-white',
176
+ classNames?.counter,
177
+ )}
178
+ >
179
+ {entries.length}
180
+ </span>
181
+ </span>
182
+
183
+ <span className="flex flex-shrink-0 items-center gap-2">
184
+ <CountPill
185
+ status="completed"
186
+ count={counts.completed}
187
+ className="bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
188
+ />
189
+ <CountPill
190
+ status="editing"
191
+ count={counts.editing}
192
+ className="bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
193
+ />
194
+ <CountPill
195
+ status="pending"
196
+ count={counts.pending}
197
+ className="bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
198
+ />
199
+ {open ? (
200
+ <ChevronUp size={15} className="flex-shrink-0 text-blue-500" aria-hidden />
201
+ ) : (
202
+ <ChevronDown size={15} className="flex-shrink-0 text-blue-500" aria-hidden />
203
+ )}
204
+ </span>
205
+ </button>
206
+
207
+ {open ? (
208
+ /**
209
+ * Teto de altura com rolagem interna, como no financiamento (`max-h-64 overflow-y-auto`).
210
+ * Sem isso o painel cresce sem limite — e cresce de verdade: o host anexa as chaves de
211
+ * contexto que ele não conhece ao fim da lista, então uma conversa com bastante estado
212
+ * empurrava a conversa inteira para fora da tela.
213
+ *
214
+ * A diferença: lá o teto envolve o cabeçalho também, então ele rola junto e sai de vista.
215
+ * Aqui só o corpo rola — o cabeçalho é o que fecha o painel, e perder o clique de fechar
216
+ * no meio da rolagem é pior do que a economia de uma linha de markup.
217
+ */
218
+ <div
219
+ className={cn(
220
+ 'cv-scrollbar-thin max-h-64 overflow-y-auto border-t border-blue-200 px-4 pb-4 pt-1 dark:border-blue-900',
221
+ classNames?.body,
222
+ )}
223
+ >
224
+ {entries.length === 0 ? (
225
+ <p className="pt-2 text-xs text-gray-500 dark:text-gray-400">{labels.empty}</p>
226
+ ) : (
227
+ /**
228
+ * Três colunas no desktop, como no financiamento — com duas, seis campos viravam uma
229
+ * coluna alta que empurrava a conversa.
230
+ *
231
+ * O que não fizemos igual: lá o painel FILTRA o que não foi coletado, porque a fonte
232
+ * só guarda o que o cliente respondeu. Aqui a lista de campos é conhecida de antemão,
233
+ * e "falta endereço" é justamente o que o atendente precisa ver ao assumir. Então o
234
+ * pendente aparece — tracejado e apagado, para ler como lacuna e não como dado.
235
+ */
236
+ <dl className="mt-2 grid grid-cols-2 gap-2 lg:grid-cols-3">
237
+ {entries.map((entry) => {
238
+ const status = statusOf(entry)
239
+
240
+ return (
241
+ <div
242
+ key={entry.key}
243
+ className={cn('min-w-0 rounded-lg p-2.5 transition-all', CARD_STYLE[status])}
244
+ >
245
+ <dt className="mb-1 flex items-center justify-center gap-1">
246
+ {entry.icon ? (
247
+ <span aria-hidden className="text-base leading-none">
248
+ {entry.icon}
249
+ </span>
250
+ ) : null}
251
+ <span className="truncate text-[10px] font-medium uppercase leading-none tracking-wide text-gray-500 dark:text-gray-400">
252
+ {entry.label}
253
+ </span>
254
+ <span className={cn('flex-shrink-0 leading-none', GLYPH_STYLE[status])}>
255
+ <StatusGlyph status={status} />
256
+ </span>
257
+ </dt>
258
+ {/* `title` no valor: truncado no card, o texto completo só existe no tooltip. */}
259
+ <dd
260
+ className={cn(
261
+ 'truncate text-center text-xs font-semibold',
262
+ entry.value
263
+ ? 'text-gray-900 dark:text-gray-100'
264
+ : 'italic font-normal text-gray-400 dark:text-gray-500',
265
+ )}
266
+ data-cv-tooltip={entry.value ?? labels.notCollected}
267
+ >
268
+ {entry.value ?? labels.notCollected}
269
+ </dd>
270
+ </div>
271
+ )
272
+ })}
273
+ </dl>
274
+ )}
275
+ </div>
276
+ ) : null}
277
+ </section>
278
+ </div>
105
279
  )
106
280
  }