diff --git a/frontend/src/components/CopyButton.tsx b/frontend/src/components/CopyButton.tsx index c8d37a8e..69285193 100644 --- a/frontend/src/components/CopyButton.tsx +++ b/frontend/src/components/CopyButton.tsx @@ -35,6 +35,9 @@ export default function CopyButton({ const { t } = useTranslation(); const [isCopied, setIsCopied] = useState(false); const timeoutIdRef = useRef(null); + // `copy` is async, so `isCopied` and the disabled prop only catch up after it + // resolves. Guard that window so rapid clicks cannot start a second write. + const copyInFlightRef = useRef(false); const iconWrapperClasses = clsx( 'flex items-center justify-center rounded-full transition-colors duration-150 ease-in-out', @@ -72,7 +75,8 @@ export default function CopyButton({ : t('conversation.copy'); const handleCopy = useCallback(async () => { - if (isCopied) return; + if (isCopied || copyInFlightRef.current) return; + copyInFlightRef.current = true; try { const success = await copy(textToCopy); @@ -92,6 +96,8 @@ export default function CopyButton({ } } catch (error) { console.error('Failed to copy text:', error); + } finally { + copyInFlightRef.current = false; } }, [textToCopy, copiedDuration, isCopied]);