feat(newwhats): frontend do plugin (Inbox/Sessions/Secretária) em tela cheia

- /wa-inbox, /wa-sessions e /wa-secretaria rodam sem a sidebar do scoreodonto
  (isStandaloneView); cada tela tem seu "Voltar" (Secretária ganhou botão na ThinNav).
- SessionsView: banner "Você já possui acesso ao WhatsApp" quando há sessão ativa.
- avatares: usa a URL do CDN (contactAvatarUrl/instance.avatar) direto; Avatar
  exibe sem depender de version; self-heal no onError re-busca a foto atual.
- deps: framer-motion, immer. Dev override com HMR (docker-compose.dev.yml).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-07-01 20:54:24 +02:00
parent 6593993b3d
commit 3042ddca38
54 changed files with 12641 additions and 34 deletions
@@ -0,0 +1,722 @@
'use client';
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react';
import {
Smile,
Paperclip,
Mic,
Send,
X,
StopCircle,
Pause,
Play,
LayoutTemplate,
BookOpen,
Plus,
Sticker,
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
// Satélite: emoji picker simples (evita a dep @emoji-mart, incompatível com react 19).
const EMOJI_SET = ['😀','😁','😂','🤣','😊','😍','😘','😎','🤔','😐','😴','😭','😡','👍','👎','🙏','👏','🙌','💪','🔥','🎉','❤️','💔','✅','❌','⚠️','📌','📎','📞','💬','⏰','🥳','😇','🤝','👀','💯'];
import { groupApi, type GroupParticipant, type MessageTemplate, type TemplateType } from '../services/chatApiService';
import RichMessageComposer from './RichMessageComposer';
import TemplateLibrary from './TemplateLibrary';
import ScheduleMessageModal from './ScheduleMessageModal';
import FilePreviewModal from './FilePreviewModal';
import StickerPanel from './StickerPanel';
import { mediaApi } from '../services/chatApiService';
interface Message {
id: number;
chat_id: number;
message_id: string;
text: string;
direction: 'in' | 'out';
}
interface InputBarProps {
newMessage: string;
onNewMessageChange: (val: string) => void;
onSend: (e?: React.FormEvent) => void;
onPresence: () => void;
onSendMedia: (file: File, caption?: string) => Promise<void>;
onSendAudio: (blob: Blob) => Promise<void>;
replyingTo: Message | null;
onCancelReply: () => void;
isMobile: boolean;
selectedChat: any;
instanceId?: string | null;
mentions?: string[];
onMentionsChange?: (mentions: string[]) => void;
}
const InputBar: React.FC<InputBarProps> = ({
newMessage,
onNewMessageChange,
onSend,
onPresence,
onSendMedia,
onSendAudio,
replyingTo,
onCancelReply,
isMobile,
selectedChat,
instanceId,
mentions = [],
onMentionsChange,
}) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const [showStickerPanel, setShowStickerPanel] = useState(false);
const [showAttachMenu, setShowAttachMenu] = useState(false);
const stickerPanelRef = useRef<HTMLDivElement>(null);
const [previewFile, setPreviewFile] = useState<File | null>(null);
const attachMenuRef = useRef<HTMLDivElement>(null);
const [showRichComposer, setShowRichComposer] = useState(false);
const [showTemplateLibrary, setShowTemplateLibrary] = useState(false);
const [richInitialPayload, setRichInitialPayload] = useState<Record<string, unknown> | undefined>();
const [richInitialType, setRichInitialType] = useState<TemplateType | undefined>();
const [showScheduleModal, setShowScheduleModal] = useState(false);
const [schedulingPayload, setSchedulingPayload] = useState<Record<string, unknown>>({});
const [schedulingType, setSchedulingType] = useState<TemplateType>('TEXT');
const emojiPickerRef = useRef<HTMLDivElement>(null);
// ── Mention autocomplete state ──────────────────────────────────────
const [mentionQuery, setMentionQuery] = useState<string | null>(null);
const [mentionIndex, setMentionIndex] = useState(0);
const [participants, setParticipants] = useState<GroupParticipant[]>([]);
const participantsCache = useRef<Record<string, GroupParticipant[]>>({});
const mentionDropdownRef = useRef<HTMLDivElement>(null);
const isGroupChat = selectedChat?.remote_jid?.endsWith('@g.us') || selectedChat?.jid?.endsWith('@g.us');
const groupJid = selectedChat?.remote_jid || selectedChat?.jid;
// Fetch group participants when group chat is selected
useEffect(() => {
if (!isGroupChat || !instanceId || !groupJid) {
setParticipants([]);
return;
}
if (participantsCache.current[groupJid]) {
setParticipants(participantsCache.current[groupJid]);
return;
}
groupApi.getParticipants(instanceId, groupJid)
.then((data) => {
participantsCache.current[groupJid] = data.participants;
setParticipants(data.participants);
})
.catch(() => setParticipants([]));
}, [isGroupChat, instanceId, groupJid]);
// Filter participants by mention query
const filteredParticipants = useMemo(() => {
if (mentionQuery === null || !isGroupChat) return [];
const q = mentionQuery.toLowerCase();
// Only show participants that have a displayable name or phone
return participants.filter((p) => {
const label = p.name || p.phone || '';
if (!label) return false;
return q === '' || label.toLowerCase().includes(q);
}).slice(0, 8);
}, [mentionQuery, participants, isGroupChat]);
// Detect @ trigger in text
const detectMentionQuery = useCallback((text: string, cursorPos: number) => {
if (!isGroupChat) { setMentionQuery(null); return; }
const before = text.slice(0, cursorPos);
const atMatch = before.match(/@([^\s@]*)$/);
if (atMatch) {
setMentionQuery(atMatch[1]);
setMentionIndex(0);
} else {
setMentionQuery(null);
}
}, [isGroupChat]);
const selectMention = useCallback((participant: GroupParticipant) => {
const textarea = textareaRef.current;
if (!textarea) return;
const cursorPos = textarea.selectionStart;
const before = newMessage.slice(0, cursorPos);
const after = newMessage.slice(cursorPos);
const atIdx = before.lastIndexOf('@');
if (atIdx === -1) return;
const displayName = participant.name || participant.phone;
const replacement = `@${displayName} `;
const newText = before.slice(0, atIdx) + replacement + after;
onNewMessageChange(newText);
setMentionQuery(null);
// Add JID to mentions list
if (onMentionsChange && !mentions.includes(participant.jid)) {
onMentionsChange([...mentions, participant.jid]);
}
// Restore cursor
requestAnimationFrame(() => {
if (textareaRef.current) {
const newCursorPos = atIdx + replacement.length;
textareaRef.current.selectionStart = newCursorPos;
textareaRef.current.selectionEnd = newCursorPos;
textareaRef.current.focus();
}
});
}, [newMessage, onNewMessageChange, mentions, onMentionsChange]);
// Audio recording states
const [isRecording, setIsRecording] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const [recordingDuration, setRecordingDuration] = useState(0);
const [micError, setMicError] = useState<string | null>(null);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const audioChunksRef = useRef<Blob[]>([]);
const timerRef = useRef<NodeJS.Timeout | null>(null);
// navigator.mediaDevices só existe em contextos seguros (HTTPS ou localhost)
const micAvailable = typeof window !== 'undefined' && !!window.isSecureContext && !!navigator.mediaDevices?.getUserMedia;
// Auto-expand textarea
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 150)}px`;
}
}, [newMessage]);
// Close emoji picker, sticker panel and attach menu on click outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (emojiPickerRef.current && !emojiPickerRef.current.contains(event.target as Node)) {
setShowEmojiPicker(false);
}
if (stickerPanelRef.current && !stickerPanelRef.current.contains(event.target as Node)) {
setShowStickerPanel(false);
}
if (attachMenuRef.current && !attachMenuRef.current.contains(event.target as Node)) {
setShowAttachMenu(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleSendSticker = useCallback(async (blob: Blob) => {
if (!instanceId || !selectedChat?.remote_jid) return;
setShowStickerPanel(false);
await mediaApi.sendSticker(instanceId, selectedChat.remote_jid, blob);
}, [instanceId, selectedChat]);
const handleKeyDown = (e: React.KeyboardEvent) => {
// Mention dropdown navigation
if (mentionQuery !== null && filteredParticipants.length > 0) {
if (e.key === 'ArrowDown') {
e.preventDefault();
setMentionIndex((prev) => (prev + 1) % filteredParticipants.length);
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
setMentionIndex((prev) => (prev - 1 + filteredParticipants.length) % filteredParticipants.length);
return;
}
if (e.key === 'Enter' || e.key === 'Tab') {
e.preventDefault();
selectMention(filteredParticipants[mentionIndex]);
return;
}
if (e.key === 'Escape') {
e.preventDefault();
setMentionQuery(null);
return;
}
}
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
onSend();
}
};
const onEmojiSelect = (emoji: any) => {
onNewMessageChange(newMessage + (emoji.native ?? ''));
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setPreviewFile(file);
if (fileInputRef.current) fileInputRef.current.value = '';
}
};
const handlePreviewConfirm = async (file: File, caption: string) => {
await onSendMedia(file, caption);
setPreviewFile(null);
};
const handlePreviewCancel = () => setPreviewFile(null);
const startRecording = async () => {
if (!micAvailable) {
setMicError('Áudio requer HTTPS. Acesse via https:// para gravar.');
setTimeout(() => setMicError(null), 4000);
return;
}
setMicError(null);
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mediaRecorder = new MediaRecorder(stream);
mediaRecorderRef.current = mediaRecorder;
audioChunksRef.current = [];
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunksRef.current.push(event.data);
}
};
mediaRecorder.onstop = async () => {
const mimeType = mediaRecorder.mimeType || 'audio/webm'
const audioBlob = new Blob(audioChunksRef.current, { type: mimeType });
await onSendAudio(audioBlob);
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.start();
setIsRecording(true);
setIsPaused(false);
setRecordingDuration(0);
timerRef.current = setInterval(() => {
setRecordingDuration(prev => prev + 1);
}, 1000);
} catch (err: any) {
const denied = err?.name === 'NotAllowedError' || err?.name === 'PermissionDeniedError';
const msg = denied
? 'Permissão de microfone negada.'
: 'Não foi possível acessar o microfone.';
setMicError(msg);
setTimeout(() => setMicError(null), 4000);
}
};
const togglePause = () => {
const mr = mediaRecorderRef.current
if (!mr) return
if (isPaused) {
mr.resume()
setIsPaused(false)
timerRef.current = setInterval(() => setRecordingDuration(prev => prev + 1), 1000)
} else {
mr.pause()
setIsPaused(true)
if (timerRef.current) clearInterval(timerRef.current)
}
}
const stopRecording = () => {
if (mediaRecorderRef.current && isRecording) {
// Se estava pausado, retoma antes de parar para capturar o último chunk
if (isPaused && mediaRecorderRef.current.state === 'paused') {
mediaRecorderRef.current.resume()
}
mediaRecorderRef.current.stop();
setIsRecording(false);
setIsPaused(false);
if (timerRef.current) clearInterval(timerRef.current);
}
};
const handleTemplateSelect = (template: MessageTemplate) => {
setRichInitialPayload(template.payload)
setRichInitialType(template.type)
setShowRichComposer(true)
}
const handleScheduleFromComposer = (type: TemplateType, payload: Record<string, unknown>) => {
setSchedulingType(type)
setSchedulingPayload(payload)
setShowRichComposer(false)
setShowScheduleModal(true)
}
const formatDuration = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
return (
<>
<div className="relative z-20">
{/* Hidden File Input */}
<input
type="file"
className="hidden"
ref={fileInputRef}
onChange={handleFileChange}
/>
{/* Emoji Picker Popover */}
<AnimatePresence>
{showEmojiPicker && (
<motion.div
ref={emojiPickerRef}
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
className="absolute bottom-20 left-4 z-50 shadow-2xl rounded-2xl overflow-hidden border border-gray-100"
>
<div className="bg-white w-[280px] p-2 grid grid-cols-8 gap-1">
{EMOJI_SET.map((e) => (
<button
key={e}
type="button"
onClick={() => onEmojiSelect({ native: e })}
className="text-xl leading-none p-1 rounded-lg hover:bg-gray-100"
>
{e}
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
{/* Sticker Panel Popover */}
<AnimatePresence>
{showStickerPanel && instanceId && (
<motion.div
ref={stickerPanelRef}
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
className="absolute bottom-20 left-16 z-50"
>
<StickerPanel
instanceId={instanceId}
jid={selectedChat?.remote_jid ?? ''}
onSend={handleSendSticker}
/>
</motion.div>
)}
</AnimatePresence>
{/* Reply Preview */}
<AnimatePresence>
{replyingTo && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
className="mx-4 mb-2 bg-white/80 backdrop-blur-sm border-l-[4px] border-[#00a884] rounded-lg px-4 py-3 flex items-start justify-between gap-4 shadow-sm"
>
<div className="min-w-0">
<div className="text-[12px] font-medium text-[#00a884] mb-1">
{replyingTo.direction === 'out' ? 'Respondendo a você' : 'Respondendo ao contato'}
</div>
<div className="text-sm text-gray-500 font-medium truncate italic">
{replyingTo.text || 'Anexo/Mídia'}
</div>
</div>
<button
type="button"
className="bg-gray-100 hover:bg-gray-200 text-gray-500 p-1 rounded-full transition-colors"
onClick={onCancelReply}
>
<X className="w-3.5 h-3.5 stroke-[3px]" />
</button>
</motion.div>
)}
</AnimatePresence>
{/* Mic error banner */}
<AnimatePresence>
{micError && (
<motion.div
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 6 }}
transition={{ duration: 0.18 }}
className="mx-4 mb-1 px-3 py-2 bg-amber-50 border border-amber-200 rounded-lg flex items-center gap-2"
>
<Mic className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />
<span className="text-xs text-amber-700">{micError}</span>
</motion.div>
)}
</AnimatePresence>
{/* Main Input Bar */}
<div className="min-h-[62px] px-4 py-2 bg-transparent flex items-center gap-2 shrink-0">
{!isRecording ? (
<>
<div className="flex items-center gap-1">
{/* Grupo flutuante: + expande Anexar / Templates / Mensagem Rica */}
<div className="relative" ref={attachMenuRef}>
<button
onClick={() => setShowAttachMenu(v => !v)}
className={`p-2 rounded-full transition-all active:scale-95 ${showAttachMenu ? 'bg-[#00a884] text-white' : 'hover:bg-black/10 text-[#54656f]'}`}
title="Mais opções"
>
<motion.div
animate={{ rotate: showAttachMenu ? 45 : 0 }}
transition={{ duration: 0.18 }}
>
<Plus className="w-[26px] h-[26px]" />
</motion.div>
</button>
<AnimatePresence>
{showAttachMenu && (
<motion.div
initial={{ opacity: 0, y: 8, scale: 0.92 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 8, scale: 0.92 }}
transition={{ duration: 0.15 }}
className="absolute bottom-full left-0 mb-2 flex flex-col items-center gap-1 bg-white rounded-2xl shadow-2xl border border-gray-100 py-2 px-1.5"
>
{/* Anexar arquivo */}
<button
onClick={() => { fileInputRef.current?.click(); setShowAttachMenu(false); }}
className="p-2.5 hover:bg-[#f0f2f5] rounded-xl transition-all text-[#54656f] active:scale-95 group relative"
title="Anexar arquivo"
>
<Paperclip className="w-[22px] h-[22px] rotate-45" />
<span className="absolute left-full ml-2 whitespace-nowrap text-xs bg-[#111b21] text-white px-2 py-1 rounded-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity">
Anexar
</span>
</button>
{/* Biblioteca de templates */}
<button
onClick={() => { setShowTemplateLibrary(true); setShowAttachMenu(false); }}
className="p-2.5 hover:bg-[#f0f2f5] rounded-xl transition-all text-[#54656f] active:scale-95 group relative"
title="Templates salvos"
>
<BookOpen className="w-[22px] h-[22px]" />
<span className="absolute left-full ml-2 whitespace-nowrap text-xs bg-[#111b21] text-white px-2 py-1 rounded-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity">
Templates
</span>
</button>
{/* Mensagem rica */}
<button
onClick={() => { setRichInitialPayload(undefined); setRichInitialType(undefined); setShowRichComposer(true); setShowAttachMenu(false); }}
className="p-2.5 hover:bg-[#f0f2f5] rounded-xl transition-all text-[#54656f] active:scale-95 group relative"
title="Mensagem rica"
>
<LayoutTemplate className="w-[22px] h-[22px]" />
<span className="absolute left-full ml-2 whitespace-nowrap text-xs bg-[#111b21] text-white px-2 py-1 rounded-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity">
Mensagem Rica
</span>
</button>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Emoji — mantém no lugar */}
<button
onClick={() => { setShowEmojiPicker(!showEmojiPicker); setShowStickerPanel(false); }}
className={`p-2 hover:bg-black/10 rounded-full transition-all active:scale-95 ${showEmojiPicker ? 'text-[#00a884]' : 'text-[#54656f]'}`}
title="Emojis"
>
<Smile className="w-[26px] h-[26px]" />
</button>
{/* Figurinhas */}
{instanceId && (
<button
onClick={() => { setShowStickerPanel(!showStickerPanel); setShowEmojiPicker(false); }}
className={`p-2 hover:bg-black/10 rounded-full transition-all active:scale-95 ${showStickerPanel ? 'text-[#00a884]' : 'text-[#54656f]'}`}
title="Figurinhas"
>
<Sticker className="w-[24px] h-[24px]" />
</button>
)}
</div>
<div className="flex-1 relative">
{/* Mention Autocomplete Dropdown */}
<AnimatePresence>
{mentionQuery !== null && filteredParticipants.length > 0 && (
<motion.div
ref={mentionDropdownRef}
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 6 }}
transition={{ duration: 0.12 }}
className="absolute bottom-full left-0 right-0 mb-1 bg-white rounded-lg shadow-xl border border-gray-200 max-h-[240px] overflow-y-auto z-50"
>
{filteredParticipants.map((p, i) => (
<button
key={p.jid}
type="button"
className={`w-full px-3 py-2 flex items-center gap-2 text-left text-sm hover:bg-[#f0f2f5] transition-colors ${
i === mentionIndex ? 'bg-[#e9edef]' : ''
}`}
onMouseDown={(e) => {
e.preventDefault();
selectMention(p);
}}
onMouseEnter={() => setMentionIndex(i)}
>
<div className="w-7 h-7 rounded-full bg-[#dfe5e7] flex items-center justify-center text-xs font-bold text-[#54656f] shrink-0">
{(p.name || p.phone || '?')[0].toUpperCase()}
</div>
<div className="min-w-0">
<div className="font-medium text-[#111b21] truncate">{p.name || p.phone}</div>
{p.name && <div className="text-[11px] text-[#667781] truncate">+{p.phone}</div>}
</div>
{p.admin && (
<span className="ml-auto text-[10px] bg-[#e7f8f2] text-[#00a884] px-1.5 py-0.5 rounded font-medium shrink-0">
{p.admin === 'superadmin' ? 'Admin' : 'Admin'}
</span>
)}
</button>
))}
</motion.div>
)}
</AnimatePresence>
<label htmlFor="message-textarea" className="sr-only">Digite sua mensagem</label>
<textarea
id="message-textarea"
name="message-textarea"
ref={textareaRef}
rows={1}
placeholder="Digite uma mensagem"
className="w-full px-3 py-2.5 bg-white rounded-lg text-[15px] text-[#111b21] border-none focus:ring-0 focus:outline-none placeholder:text-[#667781] resize-none shadow-sm transition-all overflow-hidden leading-[1.4]"
value={newMessage}
onChange={(e) => {
onNewMessageChange(e.target.value);
onPresence();
detectMentionQuery(e.target.value, e.target.selectionStart);
}}
onKeyDown={handleKeyDown}
/>
</div>
<div className="transition-all flex items-center">
{newMessage.trim() ? (
<motion.button
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
onClick={() => onSend()}
className="p-2 text-[#54656f] hover:bg-black/10 rounded-full transition-all active:scale-90"
>
<Send className="w-[26px] h-[26px]" />
</motion.button>
) : (
<button
onClick={startRecording}
title={!micAvailable ? 'Requer HTTPS para gravar áudio' : 'Gravar áudio'}
className={`p-2 rounded-full transition-all active:scale-90 ${
!micAvailable
? 'text-[#aab7bf] cursor-not-allowed opacity-50'
: 'hover:bg-black/10 text-[#54656f]'
}`}
>
<Mic className="w-[26px] h-[26px]" />
</button>
)}
</div>
</>
) : (
<div className={`flex-1 flex items-center justify-between rounded-2xl px-4 py-2 border shadow-inner ${isPaused ? 'bg-amber-50/70 border-amber-200' : 'bg-red-50/50 border-red-100'}`}>
{/* Indicador + tempo */}
<div className="flex items-center gap-3">
{isPaused ? (
<div className="w-3 h-3 bg-amber-400 rounded-full" />
) : (
<div className="w-3 h-3 bg-red-500 rounded-full animate-ping" />
)}
<span className={`text-sm font-bold tracking-wider ${isPaused ? 'text-amber-600' : 'text-red-600'}`}>
{isPaused ? 'PAUSADO' : 'GRAVANDO'} {formatDuration(recordingDuration)}
</span>
</div>
{/* Controles */}
<div className="flex items-center gap-1.5">
{/* Cancelar */}
<button
onClick={() => {
const mr = mediaRecorderRef.current
if (mr) {
mr.onstop = null;
mr.stream?.getTracks().forEach(t => t.stop());
if (mr.state !== 'inactive') mr.stop();
setIsRecording(false);
setIsPaused(false);
if (timerRef.current) clearInterval(timerRef.current);
}
}}
className="p-2 hover:bg-red-100 rounded-full text-red-400 transition-colors"
title="Cancelar"
>
<X className="w-5 h-5" />
</button>
{/* Pause / Resume */}
<button
onClick={togglePause}
className="p-2 hover:bg-black/10 rounded-full text-[#54656f] transition-colors"
title={isPaused ? 'Continuar gravação' : 'Pausar gravação'}
>
{isPaused
? <Play className="w-5 h-5 fill-current text-amber-500" />
: <Pause className="w-5 h-5 fill-current" />
}
</button>
{/* Parar e Enviar */}
<button
onClick={stopRecording}
className="p-2 bg-red-500 hover:bg-red-600 text-white rounded-full transition-all shadow-md active:scale-90"
title="Parar e Enviar"
>
<StopCircle className="w-5 h-5 fill-current" />
</button>
</div>
</div>
)}
</div>
</div>
{/* ── Modal de preview de arquivo ─────────────────────── */}
<FilePreviewModal
file={previewFile}
onConfirm={handlePreviewConfirm}
onCancel={handlePreviewCancel}
/>
{/* ── Modais de mensagem rica ────────────────────────────── */}
<RichMessageComposer
isOpen={showRichComposer}
onClose={() => setShowRichComposer(false)}
instanceId={instanceId ?? null}
jid={selectedChat?.remote_jid ?? selectedChat?.jid ?? ''}
onSent={() => setShowRichComposer(false)}
onSchedule={handleScheduleFromComposer}
initialPayload={richInitialPayload}
initialType={richInitialType}
/>
<TemplateLibrary
isOpen={showTemplateLibrary}
onClose={() => setShowTemplateLibrary(false)}
onSelect={handleTemplateSelect}
/>
<ScheduleMessageModal
isOpen={showScheduleModal}
onClose={() => setShowScheduleModal(false)}
payload={schedulingPayload}
messageType={schedulingType}
onSaved={() => setShowScheduleModal(false)}
/>
</>
);
};
export default InputBar;