import { useEffect, useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; import { useLocation } from "react-router-dom"; import { X, MessageSquarePlus, CheckCircle, AlertCircle } from "lucide-react"; import { useFeedback } from "../../hooks/useFeedback"; import { useAuth } from "../../hooks/useAuth"; import { getRecentErrorLogs } from "../../services/logService"; import { getFeedbackUserAgent, type FeedbackContext, } from "../../services/feedbackService"; const MAX_CONTENT_LENGTH = 2000; const LOGS_SUFFIX_MAX = 800; const RECENT_ERROR_LOGS_N = 20; const AUTO_CLOSE_DELAY_MS = 2000; interface FeedbackDialogProps { onClose: () => void; } export default function FeedbackDialog({ onClose }: FeedbackDialogProps) { const { t, i18n } = useTranslation(); const location = useLocation(); const { state: authState } = useAuth(); const { state: feedbackState, submit, reset } = useFeedback(); const [content, setContent] = useState(""); const [includeContext, setIncludeContext] = useState(false); const [includeLogs, setIncludeLogs] = useState(false); const [identify, setIdentify] = useState(false); const isAuthenticated = authState.status === "authenticated"; const userEmail = authState.account?.email ?? null; const trimmed = content.trim(); const isSending = feedbackState.status === "sending"; const isSuccess = feedbackState.status === "success"; const canSubmit = trimmed.length > 0 && !isSending && !isSuccess; useEffect(() => { function handleEscape(e: KeyboardEvent) { if (e.key === "Escape" && !isSending) onClose(); } document.addEventListener("keydown", handleEscape); return () => document.removeEventListener("keydown", handleEscape); }, [onClose, isSending]); // Auto-close after success useEffect(() => { if (!isSuccess) return; const timer = setTimeout(() => { onClose(); reset(); }, AUTO_CLOSE_DELAY_MS); return () => clearTimeout(timer); }, [isSuccess, onClose, reset]); const errorMessage = useMemo(() => { if (feedbackState.status !== "error" || !feedbackState.errorCode) return null; switch (feedbackState.errorCode) { case "rate_limit": return t("feedback.toast.error.429"); case "invalid": return t("feedback.toast.error.400"); case "network_error": case "server_error": default: return t("feedback.toast.error.generic"); } }, [feedbackState, t]); async function handleSubmit() { if (!canSubmit) return; // Compose content with optional logs suffix, keeping total ≤ 2000 chars let body = trimmed; if (includeLogs) { const rawLogs = getRecentErrorLogs(RECENT_ERROR_LOGS_N); if (rawLogs.length > 0) { const trimmedLogs = rawLogs.slice(-LOGS_SUFFIX_MAX); const suffix = `\n\n---\n${t("feedback.logsHeading")}\n${trimmedLogs}`; const available = MAX_CONTENT_LENGTH - body.length; if (available > suffix.length) { body = body + suffix; } else if (available > 50) { body = body + suffix.slice(0, available); } } } // Compose context if opted in let context: FeedbackContext | undefined; if (includeContext) { let userAgent = "Simpl'Résultat"; try { userAgent = await getFeedbackUserAgent(); } catch { // fall back to a basic string if the Rust helper fails } context = { page: location.pathname, locale: i18n.language, theme: document.documentElement.classList.contains("dark") ? "dark" : "light", viewport: `${window.innerWidth}x${window.innerHeight}`, userAgent, timestamp: new Date().toISOString(), }; } const userId = identify && isAuthenticated ? userEmail : null; await submit({ content: body, userId, context }); } return createPortal(