From 27f34db7778a94de6a75e38d13f9192859416923 Mon Sep 17 00:00:00 2001 From: simos Date: Tue, 8 Jul 2025 13:48:33 +0000 Subject: [PATCH] Refactor ChatInterface and MicButton components for improved scroll behavior and microphone support. Adjusted auto-scroll thresholds, added error handling for microphone access, and hid unused UI elements. --- src/components/ChatInterface.jsx | 61 ++++++++++++++------------- src/components/GitPanel.jsx | 12 +++--- src/components/MicButton.jsx | 60 +++++++++++++++++++++++++- src/components/QuickSettingsPanel.jsx | 4 +- 4 files changed, 99 insertions(+), 38 deletions(-) diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index d753c82..8e82682 100755 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -1112,15 +1112,15 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const isNearBottom = useCallback(() => { if (!scrollContainerRef.current) return false; const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current; - // Consider "near bottom" if within 100px of the bottom - return scrollHeight - scrollTop - clientHeight < 100; + // Consider "near bottom" if within 50px of the bottom + return scrollHeight - scrollTop - clientHeight < 50; }, []); // Handle scroll events to detect when user manually scrolls up const handleScroll = useCallback(() => { if (scrollContainerRef.current) { - const wasNearBottom = isNearBottom(); - setIsUserScrolledUp(!wasNearBottom); + const nearBottom = isNearBottom(); + setIsUserScrolledUp(!nearBottom); } }, [isNearBottom]); @@ -1540,13 +1540,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess }); useEffect(() => { - // Only auto-scroll to bottom when new messages arrive if: - // 1. Auto-scroll is enabled in settings - // 2. User hasn't manually scrolled up + // Auto-scroll to bottom when new messages arrive if (scrollContainerRef.current && chatMessages.length > 0) { if (autoScrollToBottom) { + // If auto-scroll is enabled, always scroll to bottom unless user has manually scrolled up if (!isUserScrolledUp) { - setTimeout(() => scrollToBottom(), 0); + setTimeout(() => scrollToBottom(), 50); // Small delay to ensure DOM is updated } } else { // When auto-scroll is disabled, preserve the visual position @@ -1564,12 +1563,15 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess } }, [chatMessages.length, isUserScrolledUp, scrollToBottom, autoScrollToBottom]); - // Scroll to bottom when component mounts with existing messages + // Scroll to bottom when component mounts with existing messages or when messages first load useEffect(() => { - if (scrollContainerRef.current && chatMessages.length > 0 && autoScrollToBottom) { - setTimeout(() => scrollToBottom(), 100); // Small delay to ensure rendering + if (scrollContainerRef.current && chatMessages.length > 0) { + // Always scroll to bottom when messages first load (user expects to see latest) + // Also reset scroll state + setIsUserScrolledUp(false); + setTimeout(() => scrollToBottom(), 200); // Longer delay to ensure full rendering } - }, [scrollToBottom, autoScrollToBottom]); + }, [chatMessages.length > 0, scrollToBottom]); // Trigger when messages first appear // Add scroll event listener to detect user scrolling useEffect(() => { @@ -1636,8 +1638,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess can_interrupt: true }); - // Always scroll to bottom when user sends a message (they're actively participating) - setTimeout(() => scrollToBottom(), 0); + // Always scroll to bottom when user sends a message and reset scroll state + setIsUserScrolledUp(false); // Reset scroll state so auto-scroll works for Claude's response + setTimeout(() => scrollToBottom(), 100); // Longer delay to ensure message is rendered // Session Protection: Mark session as active to prevent automatic project updates during conversation // This is crucial for maintaining chat state integrity. We handle two cases: @@ -1882,21 +1885,21 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess )}
- - {/* Floating scroll to bottom button */} - {isUserScrolledUp && chatMessages.length > 0 && ( - - )}
+ {/* Floating scroll to bottom button - positioned outside scrollable container */} + {isUserScrolledUp && chatMessages.length > 0 && ( + + )} + {/* Input Area - Fixed Bottom */}
)} - {/* Mic button */} -
+ {/* Mic button - HIDDEN */} +
)} - setCommitMessage(transcript)} - mode="default" - className="p-1.5" - /> +
+ setCommitMessage(transcript)} + mode="default" + className="p-1.5" + /> +
diff --git a/src/components/MicButton.jsx b/src/components/MicButton.jsx index 38df817..2e82e65 100755 --- a/src/components/MicButton.jsx +++ b/src/components/MicButton.jsx @@ -5,13 +5,35 @@ import { transcribeWithWhisper } from '../utils/whisper'; export function MicButton({ onTranscript, className = '' }) { const [state, setState] = useState('idle'); // idle, recording, transcribing, processing const [error, setError] = useState(null); + const [isSupported, setIsSupported] = useState(true); const mediaRecorderRef = useRef(null); const streamRef = useRef(null); const chunksRef = useRef([]); const lastTapRef = useRef(0); - // Version indicator to verify updates + // Check microphone support on mount + useEffect(() => { + const checkSupport = () => { + if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + setIsSupported(false); + setError('Microphone not supported. Please use HTTPS or a modern browser.'); + return; + } + + // Additional check for secure context + if (location.protocol !== 'https:' && location.hostname !== 'localhost') { + setIsSupported(false); + setError('Microphone requires HTTPS. Please use a secure connection.'); + return; + } + + setIsSupported(true); + setError(null); + }; + + checkSupport(); + }, []); // Start recording const startRecording = async () => { @@ -20,6 +42,11 @@ export function MicButton({ onTranscript, className = '' }) { setError(null); chunksRef.current = []; + // Check if getUserMedia is available + if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + throw new Error('Microphone access not available. Please use HTTPS or a supported browser.'); + } + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); streamRef.current = stream; @@ -79,7 +106,23 @@ export function MicButton({ onTranscript, className = '' }) { console.log('Recording started successfully'); } catch (err) { console.error('Failed to start recording:', err); - setError('Microphone access denied'); + + // Provide specific error messages based on error type + let errorMessage = 'Microphone access failed'; + + if (err.name === 'NotAllowedError') { + errorMessage = 'Microphone access denied. Please allow microphone permissions.'; + } else if (err.name === 'NotFoundError') { + errorMessage = 'No microphone found. Please check your audio devices.'; + } else if (err.name === 'NotSupportedError') { + errorMessage = 'Microphone not supported by this browser.'; + } else if (err.name === 'NotReadableError') { + errorMessage = 'Microphone is being used by another application.'; + } else if (err.message.includes('HTTPS')) { + errorMessage = err.message; + } + + setError(errorMessage); setState('idle'); } }; @@ -109,6 +152,11 @@ export function MicButton({ onTranscript, className = '' }) { e.stopPropagation(); } + // Don't proceed if microphone is not supported + if (!isSupported) { + return; + } + // Debounce for mobile double-tap issue const now = Date.now(); if (now - lastTapRef.current < 300) { @@ -138,6 +186,14 @@ export function MicButton({ onTranscript, className = '' }) { // Button appearance based on state const getButtonAppearance = () => { + if (!isSupported) { + return { + icon: , + className: 'bg-gray-400 cursor-not-allowed', + disabled: true + }; + } + switch (state) { case 'recording': return { diff --git a/src/components/QuickSettingsPanel.jsx b/src/components/QuickSettingsPanel.jsx index 249a54a..6a5d091 100755 --- a/src/components/QuickSettingsPanel.jsx +++ b/src/components/QuickSettingsPanel.jsx @@ -142,8 +142,8 @@ const QuickSettingsPanel = ({
- {/* Whisper Dictation Settings */} -
+ {/* Whisper Dictation Settings - HIDDEN */} +

Whisper Dictation