Repository: supabase/supabase
File: apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx (TypeScript)

Code before:
    /\bfailure\b/i,
    /\bfailing\b/i,
    /\bcrash\b/i,
    /\bcrashed\b/i,
    /\bcrashing\b/i,

    // Issue/problem keywords
    /\bissue\b/i,
    /\bissues\b/i,
    /\bproblem\b/i,
    /\bproblems\b/i,
    /\btrouble\b/i,
    /\bdifficulty\b/i,
    /\bdifficulties\b/i,

    // Support-specific phrases
    /support ticket/i,
    /contact support/i,
    /get help/i,
    /need assistance/i,
    /require assistance/i,
    /technical support/i,
  ]

  return supportPatterns.some((pattern) => pattern.test(lowerText))
}

export const FeedbackWidget = ({ onClose }: 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<string | null>(
    LOCAL_STORAGE_KEYS.FEEDBACK_WIDGET_CONTENT,
    null
  )
  const [screenshot, setScreenshot, { isSuccess }] = useLocalStorageQuery<string | null>(
    LOCAL_STORAGE_KEYS.FEEDBACK_WIDGET_SCREENSHOT,
    null
  )

  const { data: category } = useFeedbackCategoryQuery({ prompt: debouncedFeedback })

Review comment (suggestion):
I suppose [classify.ts](https://github.com/supabase/supabase/blob/776701e41c22f76d0f8286c9acecbf7566e3001b/apps/studio/pages/api/ai/feedback/classify.ts) could also be a home for the client-side pattern detection. I felt this was neater, as classification remains purely focused on ML classification.

Code after:
    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<HTMLInputElement>) => {
    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)