From 80cf2f10ee88eeaee0a0ce22d97bc65b991b5631 Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Thu, 25 Sep 2025 10:41:57 -0400 Subject: [PATCH] Add spell checking with hover tooltips to RichChatInput - Implement inline spell checking with pattern-based detection for keyboard mashing, excessive consonants, and very long words - Add hover tooltips positioned 1px above misspelled words with suggestions - Include tooltip hover persistence to allow user interaction - Add visual highlighting with bright red background for clear visibility - Use textarea-based hover detection for cross-platform compatibility - Integrate spell checking into dual-layer rich text input architecture --- ui/desktop/src/components/RichChatInput.tsx | 88 +++++++++++++++++-- .../src/components/SpellCheckTooltip.tsx | 22 ++++- 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/ui/desktop/src/components/RichChatInput.tsx b/ui/desktop/src/components/RichChatInput.tsx index 38c5d36806..f9e209dac2 100644 --- a/ui/desktop/src/components/RichChatInput.tsx +++ b/ui/desktop/src/components/RichChatInput.tsx @@ -113,6 +113,7 @@ export const RichChatInput = forwardRef(({ suggestions: string[]; wordStart: number; wordEnd: number; + isHoveringTooltip: boolean; }>({ isVisible: false, position: { x: 0, y: 0 }, @@ -120,6 +121,7 @@ export const RichChatInput = forwardRef(({ suggestions: [], wordStart: 0, wordEnd: 0, + isHoveringTooltip: false, }); // Expose methods to parent component @@ -245,7 +247,28 @@ export const RichChatInput = forwardRef(({ } }, 500); // Debounce spell check by 500ms - return () => clearTimeout(timeoutId); + + const handleTooltipEnter = () => { + console.log('🖱️ TOOLTIP ENTER: Setting isHoveringTooltip to true'); + setTooltipState(prev => ({ + ...prev, + isHoveringTooltip: true, + })); + }; + + const handleTooltipLeave = () => { + console.log('🖱️ TOOLTIP LEAVE: Setting isHoveringTooltip to false and hiding tooltip'); + setTooltipState(prev => ({ + ...prev, + isHoveringTooltip: false, + isVisible: false, + suggestions: [], + misspelledWord: '', + wordIndex: -1, + })); + }; + + return () => clearTimeout(timeoutId); }, [value, performSpellCheck]); // Parse and render content with action pills, mention pills, spell checking, and cursor @@ -489,10 +512,16 @@ export const RichChatInput = forwardRef(({ }} onMouseLeave={(e) => { console.log('🖱️ MOUSELEAVE: Mouse left misspelled word:', content); - // Add a small delay before hiding to allow clicking on tooltip + // Add a small delay before hiding to allow moving to tooltip setTimeout(() => { - setTooltip(prev => ({ ...prev, isVisible: false })); - }, 150); + setTooltip(prev => { + // Only hide if not hovering over the tooltip + if (!prev.isHoveringTooltip) { + return { ...prev, isVisible: false }; + } + return prev; + }); + }, 100); }} > {content} @@ -565,6 +594,9 @@ export const RichChatInput = forwardRef(({ }, [onChange]); const handleTextareaKeyDown = useCallback((e: React.KeyboardEvent) => { + // Hide tooltip on any key press + setTooltip(prev => ({ ...prev, isVisible: false })); + // Update cursor position on key events setTimeout(updateCursorPosition, 0); @@ -649,6 +681,8 @@ export const RichChatInput = forwardRef(({ const handleTextareaBlur = useCallback(() => { setIsFocused(false); + // Hide tooltip when input loses focus + setTooltip(prev => ({ ...prev, isVisible: false })); onBlur?.(); }, [onBlur]); @@ -695,8 +729,50 @@ export const RichChatInput = forwardRef(({ setTooltip(prev => ({ ...prev, isVisible: false })); }, [tooltip.misspelledWord]); + // Container mouse leave handler + const handleContainerMouseLeave = useCallback(() => { + console.log('🖱️ CONTAINER MOUSE LEAVE: Hiding tooltip'); + setTooltip(prev => ({ ...prev, isVisible: false })); + }, []); + + // Hide tooltip when clicking outside or when component loses focus + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (displayRef.current && !displayRef.current.contains(event.target as Node)) { + setTooltip(prev => ({ ...prev, isVisible: false })); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + // Tooltip hover handlers + const handleTooltipEnter = useCallback(() => { + console.log('🖱️ TOOLTIP ENTER: Setting isHoveringTooltip to true'); + setTooltip(prev => ({ + ...prev, + isHoveringTooltip: true, + })); + }, []); + + const handleTooltipLeave = useCallback(() => { + console.log('🖱️ TOOLTIP LEAVE: Setting isHoveringTooltip to false and hiding tooltip'); + setTooltip(prev => ({ + ...prev, + isHoveringTooltip: false, + isVisible: false, + })); + }, []); + return ( -
+
{/* Hidden textarea for actual input handling with spell check enabled */}