// @ts-nocheck import { useDebounce } from '@uidotdev/usehooks' import { LOCAL_STORAGE_KEYS, useParams } from 'common' import { AnimatePresence, motion } from 'framer-motion' import { toPng } from 'html-to-image' import { Camera, CircleCheck, Image as ImageIcon, Upload, X } from 'lucide-react' import { useRouter } from 'next/router' import { ChangeEvent, useEffect, useRef, useState } from 'react' import { toast } from 'sonner' import { Button, cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, PopoverSeparator, TextArea, } from 'ui' import { Admonition } from 'ui-patterns' import { convertB64toBlob, isLikelySupportRequest, uploadAttachment, } from './FeedbackDropdown.utils' import { SupportLink } from '@/components/interfaces/Support/SupportLink' import { InlineLinkClassName } from '@/components/ui/InlineLink' import { useFeedbackCategoryQuery } from '@/data/feedback/feedback-category' import { useSendFeedbackMutation } from '@/data/feedback/feedback-send' import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { timeout } from '@/lib/helpers' import { useProfile } from '@/lib/profile' interface FeedbackWidgetProps { onClose: () => void onSwitchToIssueOptions: () => void } export const FeedbackWidget = ({ onClose, onSwitchToIssueOptions }: FeedbackWidgetProps) => { const router = useRouter() const { profile } = useProfile() const { ref, slug } = useParams() const { data: org } = useSelectedOrganizationQuery() const uploadButtonRef = useRef(null) const [feedback, setFeedback] = useState('') const [isSending, setSending] = useState(false) const [isSavingScreenshot, setIsSavingScreenshot] = useState(false) const [isFeedbackSent, setIsFeedbackSent] = useState(false) const debouncedFeedback = useDebounce(feedback, 500) const [storedFeedback, setStoredFeedback] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.FEEDBACK_WIDGET_CONTENT, null ) const [screenshot, setScreenshot, { isSuccess }] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.FEEDBACK_WIDGET_SCREENSHOT, null ) const { data: category } = useFeedbackCategoryQuery({ prompt: debouncedFeedback }) // Use client-side heuristic for immediate feedback, AI result takes precedence when available const isLikelySupport = isLikelySupportRequest(feedback) const effectiveCategory = category ?? (isLikelySupport ? 'support' : null) const { mutate: sendEvent } = useSendEventMutation() const { mutate: submitFeedback } = useSendFeedbackMutation({ onSuccess: () => { setIsFeedbackSent(true) setFeedback('') setStoredFeedback(null) setScreenshot(null) setSending(false) }, onError: (error) => { toast.error(`Failed to submit feedback: ${error.message}`) setSending(false) }, }) const captureScreenshot = async () => { setIsSavingScreenshot(true) function filter(node: HTMLElement) { if ((node?.children ?? []).length > 0) { return node.children[0].id !== 'feedback-widget' } return true } // Give time for dropdown to close await timeout(100) toPng(document.body, { filter }) .then((dataUrl: any) => setScreenshot(dataUrl)) .catch(() => toast.error('Failed to capture screenshot')) .finally(() => setIsSavingScreenshot(false)) } const onFilesUpload = async (event: ChangeEvent) => { event.persist() const [file] = event.target.files || (event as any).dataTransfer.items const reader = new FileReader() reader.onload = function (event) { const dataUrl = event.target?.result if (typeof dataUrl === 'string') setScreenshot(dataUrl) } reader.readAsDataURL(file) event.target.value = '' } const handlePasteEvent = async () => { // [Joshen] Support pasting images via Cmd / Ctrl + V const [data] = await navigator.clipboard.read() if (screenshot === undefined && data.types[0] === 'image/png') { const blob = await data.getType('image/png') const reader = new FileReader() reader.onload = function (event) { const dataUrl = event.target?.result if (typeof dataUrl === 'string') setScreenshot(dataUrl) } reader.readAsDataURL(blob) } } const sendFeedback = async () => { if (feedback.length === 0 && screenshot !== undefined) { return toast.error('Please include a message in your feedback.') } else if (feedback.length > 0) { setSending(true) const attachmentUrl = screenshot && profile?.gotrue_id ? await uploadAttachment({ image: screenshot, userId: profile.gotrue_id, }) : undefined const formattedFeedback = attachmentUrl !== undefined ? `${feedback}\n\nAttachments:\n${attachmentUrl}` : feedback submitFeedback({ projectRef: ref, organizationSlug: slug, message: formattedFeedback, pathname: router.asPath, }) } } // Hydrate form from localStorage once it's ready; deps intentionally omit storedFeedback/screenshot // so we don't overwrite user edits when those values change after initial load. useEffect(() => { if (storedFeedback) setFeedback(storedFeedback) if (screenshot) setScreenshot(screenshot) // eslint-disable-next-line react-hooks/exhaustive-deps -- hydrate once when localStorage is ready only }, [isSuccess]) // Persist debounced input to localStorage; only re-run when debounced value changes. useEffect(() => { if (debouncedFeedback.length > 0) setStoredFeedback(debouncedFeedback) // eslint-disable-next-line react-hooks/exhaustive-deps -- setStoredFeedback is stable; only sync on debounced value }, [debouncedFeedback]) const ThanksMessageView = () => ( <>

Your feedback has been sent. Thanks!

We don’t always respond to feedback. If you need help with your project, use the button below.

) return isFeedbackSent ? ( ) : ( <>