@adatechnology/conversations-ui 0.0.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.
Files changed (65) hide show
  1. package/dist/channel/index.d.ts +2 -0
  2. package/dist/channel/index.js +0 -0
  3. package/dist/chunk-ZDURDZTM.js +199 -0
  4. package/dist/flows/index.d.ts +246 -0
  5. package/dist/flows/index.js +1110 -0
  6. package/dist/index.d.ts +542 -0
  7. package/dist/index.js +2077 -0
  8. package/dist/styles.css +11 -0
  9. package/dist/styles.d.ts +2 -0
  10. package/package.json +52 -0
  11. package/src/AudioPlayer.tsx +103 -0
  12. package/src/Avatar.tsx +63 -0
  13. package/src/ConversationListItem.tsx +147 -0
  14. package/src/ConversationLocalesProvider.tsx +76 -0
  15. package/src/DateDivider.tsx +32 -0
  16. package/src/EmojiPicker.tsx +77 -0
  17. package/src/FileIcon.tsx +33 -0
  18. package/src/Lightbox.tsx +17 -0
  19. package/src/MediaRenderer.tsx +158 -0
  20. package/src/MessageBubble.tsx +129 -0
  21. package/src/MessageComposer.tsx +211 -0
  22. package/src/MessageTail.tsx +18 -0
  23. package/src/MessageText.tsx +42 -0
  24. package/src/MessageTimestamp.tsx +22 -0
  25. package/src/SimpleEmojiPicker.tsx +72 -0
  26. package/src/StatusTicks.tsx +39 -0
  27. package/src/Toast.tsx +140 -0
  28. package/src/Wallpaper.tsx +13 -0
  29. package/src/WhatsAppMessageEditor.tsx +142 -0
  30. package/src/channel/index.ts +1 -0
  31. package/src/conversations/index.ts +1 -0
  32. package/src/flows/FlowGroupFrame.tsx +21 -0
  33. package/src/flows/FlowGroupHeader.tsx +31 -0
  34. package/src/flows/FlowMapCanvas.tsx +76 -0
  35. package/src/flows/FlowMapNode.tsx +39 -0
  36. package/src/flows/FlowNodeCard.tsx +150 -0
  37. package/src/flows/FlowNodePanel.tsx +356 -0
  38. package/src/flows/FlowPalette.tsx +137 -0
  39. package/src/flows/FlowPortalNode.tsx +30 -0
  40. package/src/flows/FlowWhatsAppPreview.tsx +67 -0
  41. package/src/flows/flowGraph.ts +391 -0
  42. package/src/flows/index.ts +55 -0
  43. package/src/flows/labels.ts +187 -0
  44. package/src/hooks/useAsyncResource.ts +38 -0
  45. package/src/hooks/useConversationContext.ts +23 -0
  46. package/src/hooks/useConversationDocuments.ts +33 -0
  47. package/src/hooks/useConversationList.ts +32 -0
  48. package/src/hooks/useConversationMessages.ts +64 -0
  49. package/src/hooks/useConversationRealtime.ts +50 -0
  50. package/src/index.ts +87 -0
  51. package/src/lib/format.ts +32 -0
  52. package/src/lib/phone.ts +26 -0
  53. package/src/lib/whatsapp-formatting.tsx +215 -0
  54. package/src/providers/ConversationsProvider.tsx +29 -0
  55. package/src/providers/types.ts +54 -0
  56. package/src/settings/TopicsForm.tsx +109 -0
  57. package/src/settings/WelcomeFarewellForm.tsx +118 -0
  58. package/src/settings/WhatsAppCreateTemplateForm.tsx +309 -0
  59. package/src/settings/WhatsAppTemplateSettingsForm.tsx +264 -0
  60. package/src/styles.css +21 -0
  61. package/src/theme.ts +30 -0
  62. package/src/types.ts +44 -0
  63. package/src/useDarkMode.ts +48 -0
  64. package/src/useWaitingNotifications.ts +64 -0
  65. package/tsconfig.json +15 -0
@@ -0,0 +1,109 @@
1
+ import type { FormEvent } from 'react'
2
+ import { Megaphone, Save } from 'lucide-react'
3
+
4
+ export interface TopicItem {
5
+ key: string
6
+ label: string
7
+ enabled: boolean
8
+ message: string
9
+ }
10
+
11
+ export interface TopicsFormLabels {
12
+ sectionTitle: string
13
+ sectionDescription: string
14
+ messagePlaceholder: string
15
+ saveButton: string
16
+ saveSuccess: string
17
+ }
18
+
19
+ const DEFAULT_LABELS: TopicsFormLabels = {
20
+ sectionTitle: 'Tópicos intermediários',
21
+ sectionDescription: 'Mensagens automáticas para assuntos específicos, disparadas quando o cliente demonstra interesse.',
22
+ messagePlaceholder: 'Digite a mensagem automática para este tópico...',
23
+ saveButton: 'Salvar tópicos',
24
+ saveSuccess: 'Tópicos salvos com sucesso.',
25
+ }
26
+
27
+ export interface TopicsFormProps {
28
+ topics: TopicItem[]
29
+ onToggle: (key: string) => void
30
+ onMessageChange: (key: string, message: string) => void
31
+ onSave: (event: FormEvent) => void
32
+ saving?: boolean
33
+ saveSuccess?: boolean
34
+ labels?: Partial<TopicsFormLabels>
35
+ }
36
+
37
+ // Paridade com financiamento-imobiliario-bot/apps/web/src/pages/MessagesPage.tsx (aba
38
+ // "bot", seção "Tópicos intermediários") — generalizado para uma lista de tópicos
39
+ // (o bot hardcoda 'consorcio'/'promocoes'; aqui o host declara os próprios).
40
+ export function TopicsForm({
41
+ topics,
42
+ onToggle,
43
+ onMessageChange,
44
+ onSave,
45
+ saving = false,
46
+ saveSuccess = false,
47
+ labels: labelsOverride,
48
+ }: TopicsFormProps) {
49
+ const labels = { ...DEFAULT_LABELS, ...labelsOverride }
50
+
51
+ return (
52
+ <section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
53
+ <div className="px-6 py-4 border-b border-gray-100 dark:border-gray-700">
54
+ <h2 className="text-sm font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2">
55
+ <Megaphone size={16} className="text-orange-600 dark:text-orange-400" />
56
+ {labels.sectionTitle}
57
+ </h2>
58
+ <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">{labels.sectionDescription}</p>
59
+ </div>
60
+
61
+ <form onSubmit={onSave} className="p-6 space-y-6">
62
+ {topics.map((topic) => (
63
+ <div key={topic.key} className="space-y-2">
64
+ <div className="flex items-start gap-4">
65
+ <div className="flex-1">
66
+ <p className="text-sm font-medium text-gray-900 dark:text-gray-100">{topic.label}</p>
67
+ </div>
68
+ <button
69
+ type="button"
70
+ onClick={() => onToggle(topic.key)}
71
+ 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 ${
72
+ topic.enabled ? 'bg-blue-600 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700'
73
+ }`}
74
+ role="switch"
75
+ aria-checked={topic.enabled}
76
+ >
77
+ <span
78
+ className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
79
+ topic.enabled ? 'translate-x-5' : 'translate-x-0'
80
+ }`}
81
+ />
82
+ </button>
83
+ </div>
84
+ <textarea
85
+ value={topic.message}
86
+ onChange={(e) => onMessageChange(topic.key, e.target.value)}
87
+ placeholder={labels.messagePlaceholder}
88
+ maxLength={300}
89
+ rows={2}
90
+ className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
91
+ />
92
+ </div>
93
+ ))}
94
+
95
+ <div className="flex items-center gap-3">
96
+ <button
97
+ type="submit"
98
+ disabled={saving}
99
+ 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"
100
+ >
101
+ <Save size={14} />
102
+ {labels.saveButton}
103
+ </button>
104
+ {saveSuccess && <span className="text-sm text-green-600 dark:text-green-400">{labels.saveSuccess}</span>}
105
+ </div>
106
+ </form>
107
+ </section>
108
+ )
109
+ }
@@ -0,0 +1,118 @@
1
+ import type { FormEvent } from 'react'
2
+ import { LogOut, Mail, Save } from 'lucide-react'
3
+ import { WhatsAppMessageEditor } from '../WhatsAppMessageEditor'
4
+
5
+ export interface WelcomeFarewellFormLabels {
6
+ sectionTitle: string
7
+ welcomeMessage: string
8
+ welcomeMessagePlaceholder: string
9
+ welcomeMessageHint: string
10
+ farewellMessage: string
11
+ farewellMessagePlaceholder: string
12
+ farewellMessageHint: string
13
+ save: string
14
+ saving: string
15
+ saveSuccess: string
16
+ }
17
+
18
+ const DEFAULT_LABELS: WelcomeFarewellFormLabels = {
19
+ sectionTitle: 'Boas-vindas e encerramento',
20
+ welcomeMessage: 'Mensagem de boas-vindas',
21
+ welcomeMessagePlaceholder: 'Olá! Sou o assistente virtual da {empresa}...',
22
+ welcomeMessageHint: 'Enviada automaticamente no primeiro contato do cliente.',
23
+ farewellMessage: 'Mensagem de encerramento',
24
+ farewellMessagePlaceholder: 'Obrigado pelo contato! Se precisar de algo, é só chamar.',
25
+ farewellMessageHint: 'Enviada quando o atendente encerra a conversa.',
26
+ save: 'Salvar',
27
+ saving: 'Salvando...',
28
+ saveSuccess: 'Mensagens salvas com sucesso.',
29
+ }
30
+
31
+ export interface WelcomeFarewellFormProps {
32
+ welcomeMessage: string
33
+ onWelcomeMessageChange: (value: string) => void
34
+ farewellMessage: string
35
+ onFarewellMessageChange: (value: string) => void
36
+ onSave: (event: FormEvent) => void
37
+ saving?: boolean
38
+ saveSuccess?: boolean
39
+ welcomePlaceholders?: readonly string[]
40
+ farewellPlaceholders?: readonly string[]
41
+ labels?: Partial<WelcomeFarewellFormLabels>
42
+ }
43
+
44
+ const DEFAULT_WELCOME_PLACEHOLDERS = ['{empresa}', '{telefone}', '{email}', '{endereco}', '{maps}'] as const
45
+ const DEFAULT_FAREWELL_PLACEHOLDERS = ['{empresa}', '{telefone}', '{email}'] as const
46
+
47
+ // Paridade com financiamento-imobiliario-bot/apps/web/src/pages/MessagesPage.tsx (aba
48
+ // "bot", seção "Boas-vindas e Encerramento") — usa o mesmo WhatsAppMessageEditor do
49
+ // pacote (T6.5), puramente apresentacional.
50
+ export function WelcomeFarewellForm({
51
+ welcomeMessage,
52
+ onWelcomeMessageChange,
53
+ farewellMessage,
54
+ onFarewellMessageChange,
55
+ onSave,
56
+ saving = false,
57
+ saveSuccess = false,
58
+ welcomePlaceholders = DEFAULT_WELCOME_PLACEHOLDERS,
59
+ farewellPlaceholders = DEFAULT_FAREWELL_PLACEHOLDERS,
60
+ labels: labelsOverride,
61
+ }: WelcomeFarewellFormProps) {
62
+ const labels = { ...DEFAULT_LABELS, ...labelsOverride }
63
+
64
+ return (
65
+ <section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
66
+ <div className="px-6 py-4 border-b border-gray-100 dark:border-gray-700">
67
+ <h2 className="text-sm font-semibold text-gray-900 dark:text-gray-100">{labels.sectionTitle}</h2>
68
+ </div>
69
+
70
+ <form onSubmit={onSave} className="p-6 space-y-5">
71
+ <div>
72
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5 flex items-center gap-1.5">
73
+ <Mail size={13} className="text-blue-600 dark:text-blue-400" />
74
+ {labels.welcomeMessage}
75
+ </label>
76
+ <WhatsAppMessageEditor
77
+ value={welcomeMessage}
78
+ onChange={onWelcomeMessageChange}
79
+ placeholder={labels.welcomeMessagePlaceholder}
80
+ placeholders={welcomePlaceholders}
81
+ />
82
+ <p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400">{labels.welcomeMessageHint}</p>
83
+ </div>
84
+
85
+ <div>
86
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5 flex items-center gap-1.5">
87
+ <LogOut size={13} className="text-blue-600 dark:text-blue-400" />
88
+ {labels.farewellMessage}
89
+ </label>
90
+ <WhatsAppMessageEditor
91
+ value={farewellMessage}
92
+ onChange={onFarewellMessageChange}
93
+ placeholder={labels.farewellMessagePlaceholder}
94
+ placeholders={farewellPlaceholders}
95
+ />
96
+ <p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400">{labels.farewellMessageHint}</p>
97
+ </div>
98
+
99
+ {saveSuccess && (
100
+ <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">
101
+ {labels.saveSuccess}
102
+ </div>
103
+ )}
104
+
105
+ <div className="flex justify-end">
106
+ <button
107
+ type="submit"
108
+ disabled={saving}
109
+ 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"
110
+ >
111
+ {saving ? <span className="w-4 h-4 border-2 border-white/40 border-t-white rounded-full animate-spin" /> : <Save size={14} />}
112
+ {saving ? labels.saving : labels.save}
113
+ </button>
114
+ </div>
115
+ </form>
116
+ </section>
117
+ )
118
+ }
@@ -0,0 +1,309 @@
1
+ import type { FormEvent } from 'react'
2
+ import { SendHorizonal } from 'lucide-react'
3
+
4
+ export type WhatsAppTemplateHeaderType = 'NONE' | 'TEXT'
5
+
6
+ export interface WhatsAppCreateTemplateState {
7
+ name: string
8
+ category: string
9
+ language: string
10
+ headerType: WhatsAppTemplateHeaderType
11
+ headerText: string
12
+ bodyText: string
13
+ footerText: string
14
+ }
15
+
16
+ export interface WhatsAppCreateTemplateResult {
17
+ ok: boolean
18
+ message: string
19
+ status?: string
20
+ }
21
+
22
+ export interface WhatsAppCreateTemplateFormLabels {
23
+ sectionTitle: string
24
+ sectionDescription: string
25
+ nameLabel: string
26
+ nameHint: string
27
+ categoryLabel: string
28
+ languageLabel: string
29
+ headerLabel: string
30
+ headerHint: string
31
+ headerNone: string
32
+ headerText: string
33
+ headerPlaceholder: string
34
+ bodyLabel: string
35
+ bodyHint: string
36
+ bodyPlaceholder: string
37
+ detectedVariables: string
38
+ variableLabel: (index: number) => string
39
+ configureHintPrefix: string
40
+ configureHintLink: string
41
+ configureHintSuffix: string
42
+ footerLabel: string
43
+ footerHint: string
44
+ footerPlaceholder: string
45
+ previewLabel: string
46
+ previewOnline: string
47
+ previewDescription: string
48
+ previewHeaderFallback: string
49
+ previewBodyPlaceholder: string
50
+ sendForApproval: string
51
+ sending: string
52
+ successStatus: (status: string) => string
53
+ }
54
+
55
+ const DEFAULT_LABELS: WhatsAppCreateTemplateFormLabels = {
56
+ sectionTitle: 'Criar novo template',
57
+ sectionDescription: 'Envia um template para aprovação da Meta.',
58
+ nameLabel: 'Nome do template',
59
+ nameHint: 'Somente letras minúsculas, números e underscore.',
60
+ categoryLabel: 'Categoria',
61
+ languageLabel: 'Idioma',
62
+ headerLabel: 'Cabeçalho',
63
+ headerHint: 'Opcional — texto fixo exibido acima do corpo.',
64
+ headerNone: 'Sem cabeçalho',
65
+ headerText: 'Texto',
66
+ headerPlaceholder: 'Ex: Olá {{1}}!',
67
+ bodyLabel: 'Corpo da mensagem',
68
+ bodyHint: 'Use {{1}}, {{2}}... para variáveis dinâmicas.',
69
+ bodyPlaceholder: 'Ex: Olá {{1}}, sua simulação para {{2}} está pronta.',
70
+ detectedVariables: 'Variáveis detectadas',
71
+ variableLabel: (index: number) => `Variável ${index}`,
72
+ configureHintPrefix: 'Configure o mapeamento em ',
73
+ configureHintLink: 'Template usado para reabrir a janela de 24h',
74
+ configureHintSuffix: ' depois de aprovado.',
75
+ footerLabel: 'Rodapé',
76
+ footerHint: 'Opcional — texto fixo exibido abaixo do corpo.',
77
+ footerPlaceholder: 'Ex: Empresa LTDA',
78
+ previewLabel: 'Prévia',
79
+ previewOnline: 'online',
80
+ previewDescription: 'Assim aparecerá para o cliente no WhatsApp.',
81
+ previewHeaderFallback: 'Cabeçalho',
82
+ previewBodyPlaceholder: 'Corpo da mensagem aparecerá aqui…',
83
+ sendForApproval: 'Enviar para aprovação',
84
+ sending: 'Enviando...',
85
+ successStatus: (status: string) => `Status: ${status}`,
86
+ }
87
+
88
+ export interface WhatsAppCreateTemplateFormProps {
89
+ value: WhatsAppCreateTemplateState
90
+ onChange: (value: WhatsAppCreateTemplateState) => void
91
+ onSubmit: (event: FormEvent) => void
92
+ submitting?: boolean
93
+ result?: WhatsAppCreateTemplateResult | null
94
+ previewCompanyName?: string
95
+ labels?: Partial<WhatsAppCreateTemplateFormLabels>
96
+ variableExamples?: readonly string[]
97
+ }
98
+
99
+ const DEFAULT_VARIABLE_EXAMPLES = ['Nome do cliente', 'Produto', 'Valor', 'Prazo', 'Banco'] as const
100
+
101
+ function detectVariables(bodyText: string): string[] {
102
+ const matches = bodyText.match(/\{\{(\d+)\}\}/g) ?? []
103
+ return [...new Set(matches.map((token) => token.replace(/[{}]/g, '')))].sort()
104
+ }
105
+
106
+ function fillPreviewTokens(text: string): string {
107
+ return text.replace(/\{\{1\}\}/g, 'João').replace(/\{\{2\}\}/g, 'Imóvel').replace(/\{\{3\}\}/g, 'R$ 1.500')
108
+ }
109
+
110
+ // Paridade com financiamento-imobiliario-bot/apps/web/src/pages/MessagesPage.tsx (aba
111
+ // "templates", seção "Criar Novo Template") — puramente apresentacional: onSubmit é o
112
+ // único ponto de rede, controlado pelo host via ConversationsApi.
113
+ export function WhatsAppCreateTemplateForm({
114
+ value,
115
+ onChange,
116
+ onSubmit,
117
+ submitting = false,
118
+ result,
119
+ previewCompanyName = 'WhatsApp Bot',
120
+ labels: labelsOverride,
121
+ variableExamples = DEFAULT_VARIABLE_EXAMPLES,
122
+ }: WhatsAppCreateTemplateFormProps) {
123
+ const labels = { ...DEFAULT_LABELS, ...labelsOverride }
124
+ const detectedVariables = detectVariables(value.bodyText)
125
+
126
+ function set<K extends keyof WhatsAppCreateTemplateState>(key: K, next: WhatsAppCreateTemplateState[K]) {
127
+ onChange({ ...value, [key]: next })
128
+ }
129
+
130
+ return (
131
+ <section className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
132
+ <div className="px-6 py-4 border-b border-gray-100 dark:border-gray-700">
133
+ <h2 className="text-sm font-semibold text-gray-900 dark:text-gray-100">{labels.sectionTitle}</h2>
134
+ <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">{labels.sectionDescription}</p>
135
+ </div>
136
+
137
+ <form onSubmit={onSubmit} className="p-6 space-y-4">
138
+ <div>
139
+ <label className="text-sm font-medium text-gray-700 dark:text-gray-300">{labels.nameLabel}</label>
140
+ <p className="text-xs text-gray-400 mb-1.5">{labels.nameHint}</p>
141
+ <input
142
+ type="text"
143
+ value={value.name}
144
+ onChange={(e) => set('name', e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, '_'))}
145
+ placeholder="reengajamento_cliente"
146
+ className="w-full border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all"
147
+ required
148
+ />
149
+ </div>
150
+
151
+ <div className="grid grid-cols-2 gap-3">
152
+ <div>
153
+ <label className="text-sm font-medium text-gray-700 dark:text-gray-300">{labels.categoryLabel}</label>
154
+ <select
155
+ value={value.category}
156
+ onChange={(e) => set('category', e.target.value)}
157
+ className="w-full border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all"
158
+ >
159
+ <option value="UTILITY">UTILITY (Transacional)</option>
160
+ <option value="MARKETING">MARKETING (Promocional)</option>
161
+ </select>
162
+ </div>
163
+ <div>
164
+ <label className="text-sm font-medium text-gray-700 dark:text-gray-300">{labels.languageLabel}</label>
165
+ <select
166
+ value={value.language}
167
+ onChange={(e) => set('language', e.target.value)}
168
+ className="w-full border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all"
169
+ >
170
+ <option value="pt_BR">Português (Brasil)</option>
171
+ <option value="en_US">Inglês (EUA)</option>
172
+ <option value="es_ES">Espanhol</option>
173
+ </select>
174
+ </div>
175
+ </div>
176
+
177
+ <div>
178
+ <label className="text-sm font-medium text-gray-700 dark:text-gray-300">{labels.headerLabel}</label>
179
+ <p className="text-xs text-gray-400 mb-1.5">{labels.headerHint}</p>
180
+ <div className="flex gap-2">
181
+ <select
182
+ value={value.headerType}
183
+ onChange={(e) => set('headerType', e.target.value as WhatsAppTemplateHeaderType)}
184
+ className="border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 transition-all"
185
+ >
186
+ <option value="NONE">{labels.headerNone}</option>
187
+ <option value="TEXT">{labels.headerText}</option>
188
+ </select>
189
+ {value.headerType === 'TEXT' && (
190
+ <input
191
+ type="text"
192
+ value={value.headerText}
193
+ onChange={(e) => set('headerText', e.target.value)}
194
+ placeholder={labels.headerPlaceholder}
195
+ maxLength={60}
196
+ className="flex-1 border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 transition-all"
197
+ />
198
+ )}
199
+ </div>
200
+ </div>
201
+
202
+ <div>
203
+ <label className="text-sm font-medium text-gray-700 dark:text-gray-300">{labels.bodyLabel}</label>
204
+ <p className="text-xs text-gray-400 mb-1.5">{labels.bodyHint}</p>
205
+ <textarea
206
+ value={value.bodyText}
207
+ onChange={(e) => set('bodyText', e.target.value)}
208
+ placeholder={labels.bodyPlaceholder}
209
+ rows={4}
210
+ className="w-full border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all resize-none placeholder:text-gray-400 dark:placeholder:text-gray-500"
211
+ required
212
+ />
213
+ {detectedVariables.length > 0 && (
214
+ <div className="mt-2 rounded-lg bg-gray-50 dark:bg-gray-700/50 border border-gray-200 dark:border-gray-600 px-3 py-2">
215
+ <p className="font-medium text-gray-500 dark:text-gray-400 mb-1.5 uppercase tracking-wide text-xs">{labels.detectedVariables}</p>
216
+ <div className="flex flex-wrap gap-2">
217
+ {detectedVariables.map((num) => (
218
+ <span key={num} className="inline-flex items-center gap-1 text-xs">
219
+ <code className="bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 px-1.5 py-0.5 rounded font-mono">{`{{${num}}}`}</code>
220
+ <span className="text-gray-400 dark:text-gray-500">= {variableExamples[Number(num) - 1] ?? labels.variableLabel(Number(num))}</span>
221
+ </span>
222
+ ))}
223
+ </div>
224
+ <p className="text-gray-400 dark:text-gray-500 mt-1.5 text-xs">
225
+ {labels.configureHintPrefix}<strong>{labels.configureHintLink}</strong>{labels.configureHintSuffix}
226
+ </p>
227
+ </div>
228
+ )}
229
+ </div>
230
+
231
+ <div>
232
+ <label className="text-sm font-medium text-gray-700 dark:text-gray-300">{labels.footerLabel}</label>
233
+ <p className="text-xs text-gray-400 mb-1.5">{labels.footerHint}</p>
234
+ <input
235
+ type="text"
236
+ value={value.footerText}
237
+ onChange={(e) => set('footerText', e.target.value)}
238
+ placeholder={labels.footerPlaceholder}
239
+ maxLength={60}
240
+ className="w-full border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all"
241
+ />
242
+ </div>
243
+
244
+ <div>
245
+ <label className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 block">{labels.previewLabel}</label>
246
+ <div className="rounded-2xl bg-[#e5ddd5] dark:bg-[#1a1a2e] border border-gray-300 dark:border-gray-600 overflow-hidden shadow-inner">
247
+ <div className="bg-[#075e54] px-4 py-2.5 flex items-center gap-2">
248
+ <div className="w-8 h-8 rounded-full bg-white/20 flex items-center justify-center text-white text-xs font-bold">
249
+ {previewCompanyName.slice(0, 2).toUpperCase()}
250
+ </div>
251
+ <div>
252
+ <p className="text-white text-xs font-semibold leading-tight">{previewCompanyName}</p>
253
+ <p className="text-white/60 text-xs">{labels.previewOnline}</p>
254
+ </div>
255
+ </div>
256
+ <div className="px-3 py-4 space-y-3" style={{ minHeight: '120px' }}>
257
+ <div className="flex justify-start" style={{ maxWidth: '85%' }}>
258
+ <div className="bg-white dark:bg-gray-700 rounded-lg rounded-tl-none px-3 py-2 shadow-sm">
259
+ <p className="text-gray-500 dark:text-gray-400 leading-tight text-sm">{labels.previewDescription}</p>
260
+ <p className="text-gray-400 dark:text-gray-500 mt-1 text-right text-[9px]">12:00</p>
261
+ </div>
262
+ </div>
263
+ <div className="flex justify-end ml-auto" style={{ maxWidth: '85%' }}>
264
+ <div className="bg-[#dcf8c6] dark:bg-[#1b5e20] rounded-lg rounded-tr-none px-3 py-2 shadow-sm">
265
+ {value.headerType === 'TEXT' && value.headerText && (
266
+ <p className="text-xs font-bold text-gray-700 dark:text-gray-200 mb-1">
267
+ {fillPreviewTokens(value.headerText) || labels.previewHeaderFallback}
268
+ </p>
269
+ )}
270
+ <p className="text-sm text-gray-800 dark:text-gray-100 leading-relaxed">
271
+ {value.bodyText ? fillPreviewTokens(value.bodyText) : <span className="text-gray-400 italic">{labels.previewBodyPlaceholder}</span>}
272
+ </p>
273
+ {value.footerText && (
274
+ <p className="text-gray-400 dark:text-gray-400 mt-1 text-xs">{value.footerText}</p>
275
+ )}
276
+ <p className="text-gray-400 dark:text-gray-400 mt-1 text-right text-[9px]">12:01 ✓</p>
277
+ </div>
278
+ </div>
279
+ </div>
280
+ </div>
281
+ </div>
282
+
283
+ {result && (
284
+ <div
285
+ className={`rounded-xl px-4 py-3 text-sm ${
286
+ result.ok
287
+ ? 'bg-green-50 dark:bg-green-950/40 border border-green-200 dark:border-green-800 text-green-700 dark:text-green-400'
288
+ : 'bg-red-50 dark:bg-red-950/40 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400'
289
+ }`}
290
+ >
291
+ {result.message}
292
+ {result.ok && result.status && <p className="text-xs mt-1 opacity-75">{labels.successStatus(result.status)}</p>}
293
+ </div>
294
+ )}
295
+
296
+ <div className="flex justify-end">
297
+ <button
298
+ type="submit"
299
+ disabled={submitting || !value.name || !value.bodyText}
300
+ className="flex items-center gap-2 bg-emerald-600 hover:bg-emerald-700 disabled:bg-emerald-400 text-white font-semibold px-5 py-2.5 rounded-xl transition-colors text-sm"
301
+ >
302
+ {submitting ? <span className="w-4 h-4 border-2 border-white/40 border-t-white rounded-full animate-spin" /> : <SendHorizonal size={14} />}
303
+ {submitting ? labels.sending : labels.sendForApproval}
304
+ </button>
305
+ </div>
306
+ </form>
307
+ </section>
308
+ )
309
+ }