@adatechnology/conversations-ui 0.1.0-rc.20 → 0.1.0-rc.21

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.
@@ -0,0 +1,189 @@
1
+ import type { FormEvent } from 'react'
2
+ import { AudioLines, Save } from 'lucide-react'
3
+ import type { TranscriptionMode } from '../types'
4
+
5
+ export interface TranscriptionSettingsFormLabels {
6
+ sectionTitle: string
7
+ enabled: string
8
+ enabledHint: string
9
+ modeTitle: string
10
+ modeAuto: string
11
+ modeAutoHint: string
12
+ modeOnDemand: string
13
+ modeOnDemandHint: string
14
+ unavailable: string
15
+ save: string
16
+ saving: string
17
+ saveSuccess: string
18
+ }
19
+
20
+ const DEFAULT_LABELS: TranscriptionSettingsFormLabels = {
21
+ sectionTitle: 'Transcrição de áudio',
22
+ enabled: 'Transcrever notas de voz',
23
+ enabledHint: 'Converte o áudio do cliente em texto, que o atendente lê e copia direto da conversa.',
24
+ modeTitle: 'Quando transcrever',
25
+ modeAuto: 'Automaticamente',
26
+ modeAutoHint: 'Toda nota de voz recebida é transcrita na hora. Mais cômodo, consome mais cota.',
27
+ modeOnDemand: 'Quando o atendente pedir',
28
+ modeOnDemandHint: 'Transcreve só ao clicar no botão da conversa. Consome cota apenas com áudio que alguém vai ler.',
29
+ unavailable: 'Transcrição não está disponível neste ambiente — fale com quem administra a instalação.',
30
+ save: 'Salvar',
31
+ saving: 'Salvando...',
32
+ saveSuccess: 'Configuração de transcrição salva.',
33
+ }
34
+
35
+ export interface TranscriptionSettingsFormProps {
36
+ enabled: boolean
37
+ onEnabledChange: (value: boolean) => void
38
+ mode: TranscriptionMode
39
+ onModeChange: (value: TranscriptionMode) => void
40
+ onSave: (event: FormEvent) => void
41
+ /**
42
+ * A capacidade existe no servidor (engine e credencial configurados)?
43
+ *
44
+ * Distinto de `enabled`: isto é "o ambiente consegue", aquilo é "esta empresa quer". Sem a
45
+ * distinção, um lojista ligaria o interruptor num ambiente sem engine e concluiria que o produto
46
+ * está quebrado — o texto nunca apareceria e nada explicaria por quê.
47
+ */
48
+ isAvailable?: boolean
49
+ saving?: boolean
50
+ saveSuccess?: boolean
51
+ labels?: Partial<TranscriptionSettingsFormLabels>
52
+ }
53
+
54
+ /**
55
+ * Interruptor de transcrição por empresa, para a tela de configurações.
56
+ *
57
+ * Puramente apresentacional, como os outros forms daqui: quem persiste é o host. O pacote não sabe a
58
+ * rota, e a política vive nas configurações do módulo (`settings.transcriptionEnabled`), não em
59
+ * variável de ambiente — mudar de ideia sobre transcrever não deveria exigir deploy.
60
+ */
61
+ export function TranscriptionSettingsForm({
62
+ enabled,
63
+ onEnabledChange,
64
+ mode,
65
+ onModeChange,
66
+ onSave,
67
+ isAvailable = true,
68
+ saving = false,
69
+ saveSuccess = false,
70
+ labels: labelsOverride,
71
+ }: TranscriptionSettingsFormProps) {
72
+ const labels = { ...DEFAULT_LABELS, ...labelsOverride }
73
+
74
+ return (
75
+ <section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
76
+ <div className="px-6 py-4 border-b border-gray-100 dark:border-gray-700">
77
+ <h2 className="text-sm font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-1.5">
78
+ <AudioLines size={14} className="text-blue-600 dark:text-blue-400" />
79
+ {labels.sectionTitle}
80
+ </h2>
81
+ </div>
82
+
83
+ <form onSubmit={onSave} className="p-6 space-y-5">
84
+ {!isAvailable && (
85
+ <div className="bg-amber-50 dark:bg-amber-950/40 border border-amber-200 dark:border-amber-800 rounded-xl px-4 py-3 text-sm text-amber-800 dark:text-amber-300">
86
+ {labels.unavailable}
87
+ </div>
88
+ )}
89
+
90
+ <label className="flex items-start gap-3 cursor-pointer">
91
+ <input
92
+ type="checkbox"
93
+ checked={enabled}
94
+ onChange={(event) => onEnabledChange(event.target.checked)}
95
+ disabled={!isAvailable}
96
+ className="mt-0.5 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:opacity-50"
97
+ />
98
+ <span>
99
+ <span className="block text-sm font-medium text-gray-700 dark:text-gray-300">{labels.enabled}</span>
100
+ <span className="block text-xs text-gray-500 dark:text-gray-400">{labels.enabledHint}</span>
101
+ </span>
102
+ </label>
103
+
104
+ {/* O modo só aparece quando ligado: escolher "quando transcrever" sem transcrever nada é uma
105
+ pergunta sem consequência, e ler as duas opções gasta atenção do operador à toa. */}
106
+ {enabled && (
107
+ <fieldset disabled={!isAvailable} className="space-y-2.5">
108
+ <legend className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{labels.modeTitle}</legend>
109
+
110
+ <ModeOption
111
+ value="onDemand"
112
+ current={mode}
113
+ onSelect={onModeChange}
114
+ title={labels.modeOnDemand}
115
+ hint={labels.modeOnDemandHint}
116
+ />
117
+ <ModeOption
118
+ value="auto"
119
+ current={mode}
120
+ onSelect={onModeChange}
121
+ title={labels.modeAuto}
122
+ hint={labels.modeAutoHint}
123
+ />
124
+ </fieldset>
125
+ )}
126
+
127
+ {saveSuccess && (
128
+ <div className="bg-green-50 dark:bg-green-950/40 border border-green-200 dark:border-green-800 rounded-xl px-4 py-3 text-sm text-green-700 dark:text-green-400">
129
+ {labels.saveSuccess}
130
+ </div>
131
+ )}
132
+
133
+ <div className="flex justify-end">
134
+ <button
135
+ type="submit"
136
+ disabled={saving || !isAvailable}
137
+ className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-semibold px-5 py-2.5 rounded-xl transition-colors text-sm"
138
+ >
139
+ {saving ? (
140
+ <span className="w-4 h-4 border-2 border-white/40 border-t-white rounded-full animate-spin" />
141
+ ) : (
142
+ <Save size={14} />
143
+ )}
144
+ {saving ? labels.saving : labels.save}
145
+ </button>
146
+ </div>
147
+ </form>
148
+ </section>
149
+ )
150
+ }
151
+
152
+ function ModeOption({
153
+ value,
154
+ current,
155
+ onSelect,
156
+ title,
157
+ hint,
158
+ }: {
159
+ value: TranscriptionMode
160
+ current: TranscriptionMode
161
+ onSelect: (value: TranscriptionMode) => void
162
+ title: string
163
+ hint: string
164
+ }) {
165
+ const isSelected = current === value
166
+
167
+ return (
168
+ <label
169
+ className={`flex items-start gap-3 cursor-pointer rounded-xl border px-4 py-3 transition-colors ${
170
+ isSelected
171
+ ? 'border-blue-500 bg-blue-50 dark:border-blue-500 dark:bg-blue-950/30'
172
+ : 'border-gray-200 hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600'
173
+ }`}
174
+ >
175
+ <input
176
+ type="radio"
177
+ name="transcription-mode"
178
+ value={value}
179
+ checked={isSelected}
180
+ onChange={() => onSelect(value)}
181
+ className="mt-0.5 h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
182
+ />
183
+ <span>
184
+ <span className="block text-sm font-medium text-gray-700 dark:text-gray-300">{title}</span>
185
+ <span className="block text-xs text-gray-500 dark:text-gray-400">{hint}</span>
186
+ </span>
187
+ </label>
188
+ )
189
+ }
package/src/styles.css CHANGED
@@ -155,3 +155,40 @@
155
155
  max-width: calc(100vw - 1.5rem);
156
156
  }
157
157
  }
158
+
159
+ /**
160
+ * Scrollbar fina — paridade com `.scrollbar-thin` de
161
+ * financiamento-imobiliario-bot/apps/web/src/index.css.
162
+ *
163
+ * Prefixo `cv-` porque o host pode ter a própria `.scrollbar-thin` (plugin do Tailwind ou CSS
164
+ * próprio): colidir no nome faria o painel herdar regra alheia sem ninguém entender por quê.
165
+ *
166
+ * Usada onde a área rola dentro de um bloco de altura limitada, como o grid de "Suas Seleções":
167
+ * a barra padrão do sistema ocupa ~15px e, num card de 3 colunas, come uma coluna inteira.
168
+ */
169
+ .cv-scrollbar-thin {
170
+ scrollbar-width: thin;
171
+ scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
172
+ }
173
+ .cv-scrollbar-thin::-webkit-scrollbar { width: 4px; height: 4px; }
174
+ .cv-scrollbar-thin::-webkit-scrollbar-track { background: transparent; }
175
+ .cv-scrollbar-thin::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.15); border-radius: 2px; }
176
+ .cv-scrollbar-thin::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.3); }
177
+ .dark .cv-scrollbar-thin { scrollbar-color: rgba(255, 255, 255, 0.15) transparent; }
178
+ .dark .cv-scrollbar-thin::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15); }
179
+ .dark .cv-scrollbar-thin::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.3); }
180
+
181
+ /**
182
+ * Mãozinha no que é clicável.
183
+ *
184
+ * O Preflight do Tailwind v3 punha `cursor: pointer` em `button`; o v4 removeu, e sem regra
185
+ * explícita o botão herda o `cursor: default` do browser — os componentes deste pacote deixam de
186
+ * parecer clicáveis num host v4. São 91 botões aqui: carimbar a classe em cada um seria ruído que o
187
+ * próximo componente esqueceria de repetir.
188
+ *
189
+ * `:where()` zera a especificidade de propósito: a regra vence o UA stylesheet (autor sempre vence)
190
+ * mas perde de qualquer regra do host, então quem quiser outro cursor não precisa de `!important`.
191
+ */
192
+ :where(button:not(:disabled), [role='button']:not(:disabled), summary) {
193
+ cursor: pointer;
194
+ }
package/src/types.ts CHANGED
@@ -84,6 +84,32 @@ export interface MessagePayload {
84
84
  * diferente de avaliado e limpo.
85
85
  */
86
86
  moderation?: { isOffensive: boolean; terms: string[] } | null
87
+ /**
88
+ * Transcrição do áudio, vinda do backend — a UI só exibe, nunca transcreve. Rodar STT no browser
89
+ * exigiria baixar modelo por aba e daria resultado diferente por versão de cliente.
90
+ *
91
+ * `null`/ausente = não avaliado, que é diferente de `'done'` com texto vazio (áudio em silêncio,
92
+ * já processado). É essa distinção que decide se o balão oferece "transcrever" ou "sem fala
93
+ * detectada".
94
+ */
95
+ transcription?: MessageTranscription | null
87
96
  isFirstInGroup?: boolean
88
97
  isLastInGroup?: boolean
89
98
  }
99
+
100
+ export type TranscriptionStatus = 'pending' | 'done' | 'failed' | 'unsupported'
101
+
102
+ /**
103
+ * Quando transcrever, escolhido nas configurações da empresa. Espelha o
104
+ * `TranscriptionMode` de `@adatechnology/meta-whatsapp-contracts`; declarado aqui para o pacote de
105
+ * UI não obrigar quem só desenha telas a instalar os contratos do backend.
106
+ */
107
+ export type TranscriptionMode = 'auto' | 'onDemand'
108
+
109
+ export interface MessageTranscription {
110
+ status: TranscriptionStatus
111
+ text?: string | null
112
+ /** ISO 639-1 ou nome do idioma, conforme o engine. Exibido como dica, não interpretado. */
113
+ language?: string | null
114
+ engine?: string | null
115
+ }