@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.
- package/dist/channel/index.d.ts +2 -0
- package/dist/channel/index.js +0 -0
- package/dist/chunk-ZDURDZTM.js +199 -0
- package/dist/flows/index.d.ts +246 -0
- package/dist/flows/index.js +1110 -0
- package/dist/index.d.ts +542 -0
- package/dist/index.js +2077 -0
- package/dist/styles.css +11 -0
- package/dist/styles.d.ts +2 -0
- package/package.json +52 -0
- package/src/AudioPlayer.tsx +103 -0
- package/src/Avatar.tsx +63 -0
- package/src/ConversationListItem.tsx +147 -0
- package/src/ConversationLocalesProvider.tsx +76 -0
- package/src/DateDivider.tsx +32 -0
- package/src/EmojiPicker.tsx +77 -0
- package/src/FileIcon.tsx +33 -0
- package/src/Lightbox.tsx +17 -0
- package/src/MediaRenderer.tsx +158 -0
- package/src/MessageBubble.tsx +129 -0
- package/src/MessageComposer.tsx +211 -0
- package/src/MessageTail.tsx +18 -0
- package/src/MessageText.tsx +42 -0
- package/src/MessageTimestamp.tsx +22 -0
- package/src/SimpleEmojiPicker.tsx +72 -0
- package/src/StatusTicks.tsx +39 -0
- package/src/Toast.tsx +140 -0
- package/src/Wallpaper.tsx +13 -0
- package/src/WhatsAppMessageEditor.tsx +142 -0
- package/src/channel/index.ts +1 -0
- package/src/conversations/index.ts +1 -0
- package/src/flows/FlowGroupFrame.tsx +21 -0
- package/src/flows/FlowGroupHeader.tsx +31 -0
- package/src/flows/FlowMapCanvas.tsx +76 -0
- package/src/flows/FlowMapNode.tsx +39 -0
- package/src/flows/FlowNodeCard.tsx +150 -0
- package/src/flows/FlowNodePanel.tsx +356 -0
- package/src/flows/FlowPalette.tsx +137 -0
- package/src/flows/FlowPortalNode.tsx +30 -0
- package/src/flows/FlowWhatsAppPreview.tsx +67 -0
- package/src/flows/flowGraph.ts +391 -0
- package/src/flows/index.ts +55 -0
- package/src/flows/labels.ts +187 -0
- package/src/hooks/useAsyncResource.ts +38 -0
- package/src/hooks/useConversationContext.ts +23 -0
- package/src/hooks/useConversationDocuments.ts +33 -0
- package/src/hooks/useConversationList.ts +32 -0
- package/src/hooks/useConversationMessages.ts +64 -0
- package/src/hooks/useConversationRealtime.ts +50 -0
- package/src/index.ts +87 -0
- package/src/lib/format.ts +32 -0
- package/src/lib/phone.ts +26 -0
- package/src/lib/whatsapp-formatting.tsx +215 -0
- package/src/providers/ConversationsProvider.tsx +29 -0
- package/src/providers/types.ts +54 -0
- package/src/settings/TopicsForm.tsx +109 -0
- package/src/settings/WelcomeFarewellForm.tsx +118 -0
- package/src/settings/WhatsAppCreateTemplateForm.tsx +309 -0
- package/src/settings/WhatsAppTemplateSettingsForm.tsx +264 -0
- package/src/styles.css +21 -0
- package/src/theme.ts +30 -0
- package/src/types.ts +44 -0
- package/src/useDarkMode.ts +48 -0
- package/src/useWaitingNotifications.ts +64 -0
- 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
|
+
}
|