From e1b4daeeb57e6e17a2b970b17c50ba0788e9903f Mon Sep 17 00:00:00 2001 From: Romuald Wandji Date: Fri, 14 Nov 2025 14:53:24 +0100 Subject: [PATCH 1/8] clean generated text from the UI --- .../Chat/Messages/Content/Parts/Text.tsx | 7 +- .../Messages/useManualCopyToClipboard.ts | 96 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 client/src/hooks/Messages/useManualCopyToClipboard.ts diff --git a/client/src/components/Chat/Messages/Content/Parts/Text.tsx b/client/src/components/Chat/Messages/Content/Parts/Text.tsx index c926622c9d..183a932cbf 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Text.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Text.tsx @@ -1,10 +1,11 @@ -import { memo, useMemo, ReactElement } from 'react'; +import { memo, useMemo, ReactElement, useRef } from 'react'; import { useRecoilValue } from 'recoil'; import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite'; import Markdown from '~/components/Chat/Messages/Content/Markdown'; import { useMessageContext } from '~/Providers'; import { cn } from '~/utils'; import store from '~/store'; +import useManualCopyWithCleanup from '~/hooks/Messages/useManualCopyToClipboard'; type TextPartProps = { text: string; @@ -22,6 +23,9 @@ const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) => const enableUserMsgMarkdown = useRecoilValue(store.enableUserMsgMarkdown); const showCursorState = useMemo(() => showCursor && isSubmitting, [showCursor, isSubmitting]); + const contentRef = useRef(null); + useManualCopyWithCleanup(contentRef, { text }); + const content: ContentType = useMemo(() => { if (!isCreatedByUser) { return ; @@ -34,6 +38,7 @@ const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) => return (
, + messageData: Partial> & { + searchResults?: { [key: string]: SearchResultData }; + }, +) { + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const handleManualCopy = (e: ClipboardEvent) => { + // Get the selected content + const selection = window.getSelection(); + if (!selection || selection.toString().length === 0) return; + + // Create a temporary container for the selected content + const tempDiv = document.createElement('div'); + const range = selection.getRangeAt(0); + const clonedSelection = range.cloneContents(); + tempDiv.appendChild(clonedSelection); + + const stripInlineStyles = (element: Element) => { + // Remove style attribute (the main culprit for bloat) + element.removeAttribute('style'); + + // Keep essential classes, remove styling classes + const classList = element.getAttribute('class'); + if (classList) { + const essentialClasses = classList + .split(' ') + .filter( + (cls) => + !cls.includes('prose') && + !cls.includes('dark:') && + !cls.includes('light') && + !cls.startsWith('text-') && + !cls.startsWith('bg-') && + !cls.startsWith('border-') && + !cls.startsWith('shadow-') && + !cls.startsWith('rounded-') && + cls.trim().length > 0, + ) + .join(' '); + + if (essentialClasses.length > 0) { + element.setAttribute('class', essentialClasses); + } else { + element.removeAttribute('class'); + } + } + + // Recursively process all child elements + Array.from(element.children).forEach((child) => { + stripInlineStyles(child as Element); + }); + }; + + // Strip inline styles from cloned content + stripInlineStyles(tempDiv); + const cleanHtml = tempDiv.innerHTML; + + // === STEP 2: PLAIN TEXT VERSION === + // Get the plain text from selection + const selectedPlainText = selection.toString(); + + // Apply the same cleanup as useCopyToClipboard + const cleanedText = selectedPlainText + .replace(INVALID_CITATION_REGEX, '') + .replace(CLEANUP_REGEX, ''); + + // Prevent default copy behavior + e.preventDefault(); + + // Set BOTH formats to clipboard + const clipboardData = e.clipboardData; + if (clipboardData) { + // Primary format: Clean HTML (for pasting into rich text editors) + clipboardData.setData('text/html', cleanHtml); + + // Secondary format: Clean plain text (for pasting into plain text editors) + clipboardData.setData('text/plain', cleanedText); + } + }; + + container.addEventListener('copy', handleManualCopy); + + return () => { + container.removeEventListener('copy', handleManualCopy); + }; + }, [messageData.text, messageData.content, messageData.searchResults]); +} From c34c6505a5732afbf93d3799e535444b80fe0581 Mon Sep 17 00:00:00 2001 From: Romuald Wandji Date: Fri, 14 Nov 2025 16:02:51 +0100 Subject: [PATCH 2/8] added hook that clean copied text --- client/src/components/Chat/Messages/Content/Parts/Text.tsx | 4 ++-- client/src/hooks/Messages/useManualCopyToClipboard.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/components/Chat/Messages/Content/Parts/Text.tsx b/client/src/components/Chat/Messages/Content/Parts/Text.tsx index 183a932cbf..01dc2594c4 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Text.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Text.tsx @@ -5,7 +5,7 @@ import Markdown from '~/components/Chat/Messages/Content/Markdown'; import { useMessageContext } from '~/Providers'; import { cn } from '~/utils'; import store from '~/store'; -import useManualCopyWithCleanup from '~/hooks/Messages/useManualCopyToClipboard'; +import useManualCopyToClipboard from '~/hooks/Messages/useManualCopyToClipboard'; type TextPartProps = { text: string; @@ -24,7 +24,7 @@ const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) => const showCursorState = useMemo(() => showCursor && isSubmitting, [showCursor, isSubmitting]); const contentRef = useRef(null); - useManualCopyWithCleanup(contentRef, { text }); + useManualCopyToClipboard(contentRef, { text }); const content: ContentType = useMemo(() => { if (!isCreatedByUser) { diff --git a/client/src/hooks/Messages/useManualCopyToClipboard.ts b/client/src/hooks/Messages/useManualCopyToClipboard.ts index 2a86a004e4..cc643a9067 100644 --- a/client/src/hooks/Messages/useManualCopyToClipboard.ts +++ b/client/src/hooks/Messages/useManualCopyToClipboard.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { CLEANUP_REGEX, INVALID_CITATION_REGEX } from '~/utils/citations'; import type { TMessage, SearchResultData } from 'librechat-data-provider'; -export default function useManualCopyWithCleanup( +export default function useManualCopyToClipboard( containerRef: React.RefObject, messageData: Partial> & { searchResults?: { [key: string]: SearchResultData }; From 597dec93409fdf051de083eba4bc29269fb9630b Mon Sep 17 00:00:00 2001 From: Romuald Wandji Date: Mon, 17 Nov 2025 09:46:03 +0100 Subject: [PATCH 3/8] fixed eslint error --- client/src/hooks/Messages/useManualCopyToClipboard.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/src/hooks/Messages/useManualCopyToClipboard.ts b/client/src/hooks/Messages/useManualCopyToClipboard.ts index cc643a9067..e4d80c8fd1 100644 --- a/client/src/hooks/Messages/useManualCopyToClipboard.ts +++ b/client/src/hooks/Messages/useManualCopyToClipboard.ts @@ -8,7 +8,6 @@ export default function useManualCopyToClipboard( searchResults?: { [key: string]: SearchResultData }; }, ) { - useEffect(() => { const container = containerRef.current; if (!container) return; @@ -81,7 +80,7 @@ export default function useManualCopyToClipboard( if (clipboardData) { // Primary format: Clean HTML (for pasting into rich text editors) clipboardData.setData('text/html', cleanHtml); - + // Secondary format: Clean plain text (for pasting into plain text editors) clipboardData.setData('text/plain', cleanedText); } @@ -92,5 +91,5 @@ export default function useManualCopyToClipboard( return () => { container.removeEventListener('copy', handleManualCopy); }; - }, [messageData.text, messageData.content, messageData.searchResults]); + }, [containerRef, messageData.text, messageData.content, messageData.searchResults]); } From c43fd083a4ebb76e1dc33e84d5cc996d30c1cde3 Mon Sep 17 00:00:00 2001 From: Romuald Wandji Date: Fri, 14 Nov 2025 14:53:24 +0100 Subject: [PATCH 4/8] clean generated text from the UI --- .../Chat/Messages/Content/Parts/Text.tsx | 7 +- .../Messages/useManualCopyToClipboard.ts | 96 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 client/src/hooks/Messages/useManualCopyToClipboard.ts diff --git a/client/src/components/Chat/Messages/Content/Parts/Text.tsx b/client/src/components/Chat/Messages/Content/Parts/Text.tsx index c926622c9d..183a932cbf 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Text.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Text.tsx @@ -1,10 +1,11 @@ -import { memo, useMemo, ReactElement } from 'react'; +import { memo, useMemo, ReactElement, useRef } from 'react'; import { useRecoilValue } from 'recoil'; import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite'; import Markdown from '~/components/Chat/Messages/Content/Markdown'; import { useMessageContext } from '~/Providers'; import { cn } from '~/utils'; import store from '~/store'; +import useManualCopyWithCleanup from '~/hooks/Messages/useManualCopyToClipboard'; type TextPartProps = { text: string; @@ -22,6 +23,9 @@ const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) => const enableUserMsgMarkdown = useRecoilValue(store.enableUserMsgMarkdown); const showCursorState = useMemo(() => showCursor && isSubmitting, [showCursor, isSubmitting]); + const contentRef = useRef(null); + useManualCopyWithCleanup(contentRef, { text }); + const content: ContentType = useMemo(() => { if (!isCreatedByUser) { return ; @@ -34,6 +38,7 @@ const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) => return (
, + messageData: Partial> & { + searchResults?: { [key: string]: SearchResultData }; + }, +) { + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const handleManualCopy = (e: ClipboardEvent) => { + // Get the selected content + const selection = window.getSelection(); + if (!selection || selection.toString().length === 0) return; + + // Create a temporary container for the selected content + const tempDiv = document.createElement('div'); + const range = selection.getRangeAt(0); + const clonedSelection = range.cloneContents(); + tempDiv.appendChild(clonedSelection); + + const stripInlineStyles = (element: Element) => { + // Remove style attribute (the main culprit for bloat) + element.removeAttribute('style'); + + // Keep essential classes, remove styling classes + const classList = element.getAttribute('class'); + if (classList) { + const essentialClasses = classList + .split(' ') + .filter( + (cls) => + !cls.includes('prose') && + !cls.includes('dark:') && + !cls.includes('light') && + !cls.startsWith('text-') && + !cls.startsWith('bg-') && + !cls.startsWith('border-') && + !cls.startsWith('shadow-') && + !cls.startsWith('rounded-') && + cls.trim().length > 0, + ) + .join(' '); + + if (essentialClasses.length > 0) { + element.setAttribute('class', essentialClasses); + } else { + element.removeAttribute('class'); + } + } + + // Recursively process all child elements + Array.from(element.children).forEach((child) => { + stripInlineStyles(child as Element); + }); + }; + + // Strip inline styles from cloned content + stripInlineStyles(tempDiv); + const cleanHtml = tempDiv.innerHTML; + + // === STEP 2: PLAIN TEXT VERSION === + // Get the plain text from selection + const selectedPlainText = selection.toString(); + + // Apply the same cleanup as useCopyToClipboard + const cleanedText = selectedPlainText + .replace(INVALID_CITATION_REGEX, '') + .replace(CLEANUP_REGEX, ''); + + // Prevent default copy behavior + e.preventDefault(); + + // Set BOTH formats to clipboard + const clipboardData = e.clipboardData; + if (clipboardData) { + // Primary format: Clean HTML (for pasting into rich text editors) + clipboardData.setData('text/html', cleanHtml); + + // Secondary format: Clean plain text (for pasting into plain text editors) + clipboardData.setData('text/plain', cleanedText); + } + }; + + container.addEventListener('copy', handleManualCopy); + + return () => { + container.removeEventListener('copy', handleManualCopy); + }; + }, [messageData.text, messageData.content, messageData.searchResults]); +} From cde0b980cfe66db6be75a6ee994b7b420c4b51ea Mon Sep 17 00:00:00 2001 From: Romuald Wandji Date: Fri, 14 Nov 2025 16:02:51 +0100 Subject: [PATCH 5/8] added hook that clean copied text --- client/src/components/Chat/Messages/Content/Parts/Text.tsx | 4 ++-- client/src/hooks/Messages/useManualCopyToClipboard.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/components/Chat/Messages/Content/Parts/Text.tsx b/client/src/components/Chat/Messages/Content/Parts/Text.tsx index 183a932cbf..01dc2594c4 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Text.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Text.tsx @@ -5,7 +5,7 @@ import Markdown from '~/components/Chat/Messages/Content/Markdown'; import { useMessageContext } from '~/Providers'; import { cn } from '~/utils'; import store from '~/store'; -import useManualCopyWithCleanup from '~/hooks/Messages/useManualCopyToClipboard'; +import useManualCopyToClipboard from '~/hooks/Messages/useManualCopyToClipboard'; type TextPartProps = { text: string; @@ -24,7 +24,7 @@ const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) => const showCursorState = useMemo(() => showCursor && isSubmitting, [showCursor, isSubmitting]); const contentRef = useRef(null); - useManualCopyWithCleanup(contentRef, { text }); + useManualCopyToClipboard(contentRef, { text }); const content: ContentType = useMemo(() => { if (!isCreatedByUser) { diff --git a/client/src/hooks/Messages/useManualCopyToClipboard.ts b/client/src/hooks/Messages/useManualCopyToClipboard.ts index 2a86a004e4..cc643a9067 100644 --- a/client/src/hooks/Messages/useManualCopyToClipboard.ts +++ b/client/src/hooks/Messages/useManualCopyToClipboard.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { CLEANUP_REGEX, INVALID_CITATION_REGEX } from '~/utils/citations'; import type { TMessage, SearchResultData } from 'librechat-data-provider'; -export default function useManualCopyWithCleanup( +export default function useManualCopyToClipboard( containerRef: React.RefObject, messageData: Partial> & { searchResults?: { [key: string]: SearchResultData }; From ed2ca159c6955c133e0837858152a407ae874fff Mon Sep 17 00:00:00 2001 From: Romuald Wandji Date: Mon, 17 Nov 2025 09:46:03 +0100 Subject: [PATCH 6/8] fixed eslint error --- client/src/hooks/Messages/useManualCopyToClipboard.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/src/hooks/Messages/useManualCopyToClipboard.ts b/client/src/hooks/Messages/useManualCopyToClipboard.ts index cc643a9067..e4d80c8fd1 100644 --- a/client/src/hooks/Messages/useManualCopyToClipboard.ts +++ b/client/src/hooks/Messages/useManualCopyToClipboard.ts @@ -8,7 +8,6 @@ export default function useManualCopyToClipboard( searchResults?: { [key: string]: SearchResultData }; }, ) { - useEffect(() => { const container = containerRef.current; if (!container) return; @@ -81,7 +80,7 @@ export default function useManualCopyToClipboard( if (clipboardData) { // Primary format: Clean HTML (for pasting into rich text editors) clipboardData.setData('text/html', cleanHtml); - + // Secondary format: Clean plain text (for pasting into plain text editors) clipboardData.setData('text/plain', cleanedText); } @@ -92,5 +91,5 @@ export default function useManualCopyToClipboard( return () => { container.removeEventListener('copy', handleManualCopy); }; - }, [messageData.text, messageData.content, messageData.searchResults]); + }, [containerRef, messageData.text, messageData.content, messageData.searchResults]); } From 92fa9eece7fefa51f774dfed2142c5c59966e072 Mon Sep 17 00:00:00 2001 From: Romuald Wandji Date: Tue, 25 Nov 2025 15:57:21 +0100 Subject: [PATCH 7/8] clean the hook --- client/src/hooks/Messages/useManualCopyToClipboard.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/src/hooks/Messages/useManualCopyToClipboard.ts b/client/src/hooks/Messages/useManualCopyToClipboard.ts index e4d80c8fd1..bce0687578 100644 --- a/client/src/hooks/Messages/useManualCopyToClipboard.ts +++ b/client/src/hooks/Messages/useManualCopyToClipboard.ts @@ -19,9 +19,11 @@ export default function useManualCopyToClipboard( // Create a temporary container for the selected content const tempDiv = document.createElement('div'); - const range = selection.getRangeAt(0); - const clonedSelection = range.cloneContents(); - tempDiv.appendChild(clonedSelection); + const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null; + const clonedSelection = range ? range.cloneContents() : null; + if (clonedSelection) { + tempDiv.appendChild(clonedSelection); + } const stripInlineStyles = (element: Element) => { // Remove style attribute (the main culprit for bloat) From ae6a4cd0fe1bf26fff236a2dc52298e7f01ce058 Mon Sep 17 00:00:00 2001 From: Romuald Wandji Date: Tue, 25 Nov 2025 16:03:18 +0100 Subject: [PATCH 8/8] cleaned the added hook --- client/src/hooks/Messages/useManualCopyToClipboard.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/client/src/hooks/Messages/useManualCopyToClipboard.ts b/client/src/hooks/Messages/useManualCopyToClipboard.ts index 839a9078ec..896d53ba91 100644 --- a/client/src/hooks/Messages/useManualCopyToClipboard.ts +++ b/client/src/hooks/Messages/useManualCopyToClipboard.ts @@ -19,17 +19,11 @@ export default function useManualCopyToClipboard( // Create a temporary container for the selected content const tempDiv = document.createElement('div'); -<<<<<<< HEAD const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null; const clonedSelection = range ? range.cloneContents() : null; if (clonedSelection) { tempDiv.appendChild(clonedSelection); } -======= - const range = selection.getRangeAt(0); - const clonedSelection = range.cloneContents(); - tempDiv.appendChild(clonedSelection); ->>>>>>> d8de1fcd4f2f1b2de302378d16e5dcf6cfdf9ceb const stripInlineStyles = (element: Element) => { // Remove style attribute (the main culprit for bloat) @@ -99,5 +93,5 @@ export default function useManualCopyToClipboard( return () => { container.removeEventListener('copy', handleManualCopy); }; - }, [containerRef, messageData.text, messageData.content, messageData.searchResults]); + }, [containerRef]); }