import { useCallback, useEffect, useRef, useState } from 'react'; import { ImagePlus, Loader2, Paperclip, Send, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { httpClient } from '@/app/infra/http/HttpClient'; const MAX_ATTACHMENTS = 3; const MAX_IMAGE_BYTES = 1024 * 1024; type FeedbackAttachment = { name: string; mime_type: string; data_url: string; }; function readImageFile(file: File): Promise { return new Promise((resolve, reject) => { if (!file.type.startsWith('image/')) { reject(new Error('not_image')); return; } if (file.size > MAX_IMAGE_BYTES) { reject(new Error('too_large')); return; } const reader = new FileReader(); reader.onload = () => { const dataUrl = String(reader.result || ''); if (!dataUrl.startsWith('data:image/')) { reject(new Error('not_image')); return; } resolve({ name: file.name || 'pasted-image.png', mime_type: file.type || 'image/png', data_url: dataUrl, }); }; reader.onerror = () => reject(reader.error || new Error('read_failed')); reader.readAsDataURL(file); }); } const FEEDBACK_I18N_PREFIX = 'monitoring.feedback'; export function FeedbackPopoverContent({ onSubmitted, }: { onSubmitted?: () => void; }) { const { t } = useTranslation(); const tf = useCallback( (key: string) => t(`${FEEDBACK_I18N_PREFIX}.${key}`), [t], ); const [content, setContent] = useState(''); const [attachments, setAttachments] = useState([]); const [submitting, setSubmitting] = useState(false); const fileInputRef = useRef(null); const addFiles = useCallback( async (files: File[]) => { const slots = MAX_ATTACHMENTS - attachments.length; if (slots <= 0) { toast.error(tf('tooManyImages')); return; } const picked = files.slice(0, slots); const next: FeedbackAttachment[] = []; for (const file of picked) { try { next.push(await readImageFile(file)); } catch (error) { const msg = error instanceof Error ? error.message : ''; toast.error( msg === 'too_large' ? tf('imageTooLarge') : tf('imageOnly'), ); } } if (next.length > 0) { setAttachments((prev) => [...prev, ...next].slice(0, MAX_ATTACHMENTS)); } }, [attachments.length, tf], ); useEffect(() => { const onPaste = (event: ClipboardEvent) => { const files = Array.from(event.clipboardData?.files || []).filter( (file) => file.type.startsWith('image/'), ); if (files.length > 0) { event.preventDefault(); void addFiles(files); } }; window.addEventListener('paste', onPaste); return () => window.removeEventListener('paste', onPaste); }, [addFiles]); const handleSubmit = async () => { const trimmed = content.trim(); if (!trimmed) { toast.error(tf('contentRequired')); return; } try { setSubmitting(true); await httpClient.submitFeedback({ content: trimmed, attachments, }); toast.success(tf('submitSuccess')); setContent(''); setAttachments([]); onSubmitted?.(); } catch { toast.error(tf('submitFailed')); } finally { setSubmitting(false); } }; return (
e.stopPropagation()}>
{tf('title')}

{tf('description')}