Skip to content

Commit 27f34db

Browse files
committed
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.
1 parent fca741a commit 27f34db

File tree

4 files changed

+99
-38
lines changed

4 files changed

+99
-38
lines changed

src/components/ChatInterface.jsx

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,15 +1112,15 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
11121112
const isNearBottom = useCallback(() => {
11131113
if (!scrollContainerRef.current) return false;
11141114
const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
1115-
// Consider "near bottom" if within 100px of the bottom
1116-
return scrollHeight - scrollTop - clientHeight < 100;
1115+
// Consider "near bottom" if within 50px of the bottom
1116+
return scrollHeight - scrollTop - clientHeight < 50;
11171117
}, []);
11181118

11191119
// Handle scroll events to detect when user manually scrolls up
11201120
const handleScroll = useCallback(() => {
11211121
if (scrollContainerRef.current) {
1122-
const wasNearBottom = isNearBottom();
1123-
setIsUserScrolledUp(!wasNearBottom);
1122+
const nearBottom = isNearBottom();
1123+
setIsUserScrolledUp(!nearBottom);
11241124
}
11251125
}, [isNearBottom]);
11261126

@@ -1540,13 +1540,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
15401540
});
15411541

15421542
useEffect(() => {
1543-
// Only auto-scroll to bottom when new messages arrive if:
1544-
// 1. Auto-scroll is enabled in settings
1545-
// 2. User hasn't manually scrolled up
1543+
// Auto-scroll to bottom when new messages arrive
15461544
if (scrollContainerRef.current && chatMessages.length > 0) {
15471545
if (autoScrollToBottom) {
1546+
// If auto-scroll is enabled, always scroll to bottom unless user has manually scrolled up
15481547
if (!isUserScrolledUp) {
1549-
setTimeout(() => scrollToBottom(), 0);
1548+
setTimeout(() => scrollToBottom(), 50); // Small delay to ensure DOM is updated
15501549
}
15511550
} else {
15521551
// When auto-scroll is disabled, preserve the visual position
@@ -1564,12 +1563,15 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
15641563
}
15651564
}, [chatMessages.length, isUserScrolledUp, scrollToBottom, autoScrollToBottom]);
15661565

1567-
// Scroll to bottom when component mounts with existing messages
1566+
// Scroll to bottom when component mounts with existing messages or when messages first load
15681567
useEffect(() => {
1569-
if (scrollContainerRef.current && chatMessages.length > 0 && autoScrollToBottom) {
1570-
setTimeout(() => scrollToBottom(), 100); // Small delay to ensure rendering
1568+
if (scrollContainerRef.current && chatMessages.length > 0) {
1569+
// Always scroll to bottom when messages first load (user expects to see latest)
1570+
// Also reset scroll state
1571+
setIsUserScrolledUp(false);
1572+
setTimeout(() => scrollToBottom(), 200); // Longer delay to ensure full rendering
15711573
}
1572-
}, [scrollToBottom, autoScrollToBottom]);
1574+
}, [chatMessages.length > 0, scrollToBottom]); // Trigger when messages first appear
15731575

15741576
// Add scroll event listener to detect user scrolling
15751577
useEffect(() => {
@@ -1636,8 +1638,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
16361638
can_interrupt: true
16371639
});
16381640

1639-
// Always scroll to bottom when user sends a message (they're actively participating)
1640-
setTimeout(() => scrollToBottom(), 0);
1641+
// Always scroll to bottom when user sends a message and reset scroll state
1642+
setIsUserScrolledUp(false); // Reset scroll state so auto-scroll works for Claude's response
1643+
setTimeout(() => scrollToBottom(), 100); // Longer delay to ensure message is rendered
16411644

16421645
// Session Protection: Mark session as active to prevent automatic project updates during conversation
16431646
// This is crucial for maintaining chat state integrity. We handle two cases:
@@ -1882,21 +1885,21 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
18821885
)}
18831886

18841887
<div ref={messagesEndRef} />
1885-
1886-
{/* Floating scroll to bottom button */}
1887-
{isUserScrolledUp && chatMessages.length > 0 && (
1888-
<button
1889-
onClick={scrollToBottom}
1890-
className="absolute bottom-4 right-4 w-10 h-10 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all duration-200 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:ring-offset-gray-800 z-10"
1891-
title="Scroll to bottom"
1892-
>
1893-
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1894-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
1895-
</svg>
1896-
</button>
1897-
)}
18981888
</div>
18991889

1890+
{/* Floating scroll to bottom button - positioned outside scrollable container */}
1891+
{isUserScrolledUp && chatMessages.length > 0 && (
1892+
<button
1893+
onClick={scrollToBottom}
1894+
className="fixed bottom-20 sm:bottom-24 right-4 sm:right-6 w-12 h-12 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all duration-200 hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:ring-offset-gray-800 z-50"
1895+
title="Scroll to bottom"
1896+
>
1897+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1898+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
1899+
</svg>
1900+
</button>
1901+
)}
1902+
19001903
{/* Input Area - Fixed Bottom */}
19011904
<div className={`p-2 sm:p-4 md:p-6 flex-shrink-0 ${
19021905
isInputFocused ? 'pb-2 sm:pb-4 md:pb-6' : 'pb-16 sm:pb-4 md:pb-6'
@@ -1977,8 +1980,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
19771980
</svg>
19781981
</button>
19791982
)}
1980-
{/* Mic button */}
1981-
<div className="absolute right-16 sm:right-16 top-1/2 transform -translate-y-1/2">
1983+
{/* Mic button - HIDDEN */}
1984+
<div className="absolute right-16 sm:right-16 top-1/2 transform -translate-y-1/2" style={{ display: 'none' }}>
19821985
<MicButton
19831986
onTranscript={handleTranscript}
19841987
className="w-10 h-10 sm:w-10 sm:h-10"

src/components/GitPanel.jsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -568,11 +568,13 @@ function GitPanel({ selectedProject, isMobile }) {
568568
<Sparkles className="w-4 h-4" />
569569
)}
570570
</button>
571-
<MicButton
572-
onTranscript={(transcript) => setCommitMessage(transcript)}
573-
mode="default"
574-
className="p-1.5"
575-
/>
571+
<div style={{ display: 'none' }}>
572+
<MicButton
573+
onTranscript={(transcript) => setCommitMessage(transcript)}
574+
mode="default"
575+
className="p-1.5"
576+
/>
577+
</div>
576578
</div>
577579
</div>
578580
<div className="flex items-center justify-between mt-2">

src/components/MicButton.jsx

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,35 @@ import { transcribeWithWhisper } from '../utils/whisper';
55
export function MicButton({ onTranscript, className = '' }) {
66
const [state, setState] = useState('idle'); // idle, recording, transcribing, processing
77
const [error, setError] = useState(null);
8+
const [isSupported, setIsSupported] = useState(true);
89

910
const mediaRecorderRef = useRef(null);
1011
const streamRef = useRef(null);
1112
const chunksRef = useRef([]);
1213
const lastTapRef = useRef(0);
1314

14-
// Version indicator to verify updates
15+
// Check microphone support on mount
16+
useEffect(() => {
17+
const checkSupport = () => {
18+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
19+
setIsSupported(false);
20+
setError('Microphone not supported. Please use HTTPS or a modern browser.');
21+
return;
22+
}
23+
24+
// Additional check for secure context
25+
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
26+
setIsSupported(false);
27+
setError('Microphone requires HTTPS. Please use a secure connection.');
28+
return;
29+
}
30+
31+
setIsSupported(true);
32+
setError(null);
33+
};
34+
35+
checkSupport();
36+
}, []);
1537

1638
// Start recording
1739
const startRecording = async () => {
@@ -20,6 +42,11 @@ export function MicButton({ onTranscript, className = '' }) {
2042
setError(null);
2143
chunksRef.current = [];
2244

45+
// Check if getUserMedia is available
46+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
47+
throw new Error('Microphone access not available. Please use HTTPS or a supported browser.');
48+
}
49+
2350
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
2451
streamRef.current = stream;
2552

@@ -79,7 +106,23 @@ export function MicButton({ onTranscript, className = '' }) {
79106
console.log('Recording started successfully');
80107
} catch (err) {
81108
console.error('Failed to start recording:', err);
82-
setError('Microphone access denied');
109+
110+
// Provide specific error messages based on error type
111+
let errorMessage = 'Microphone access failed';
112+
113+
if (err.name === 'NotAllowedError') {
114+
errorMessage = 'Microphone access denied. Please allow microphone permissions.';
115+
} else if (err.name === 'NotFoundError') {
116+
errorMessage = 'No microphone found. Please check your audio devices.';
117+
} else if (err.name === 'NotSupportedError') {
118+
errorMessage = 'Microphone not supported by this browser.';
119+
} else if (err.name === 'NotReadableError') {
120+
errorMessage = 'Microphone is being used by another application.';
121+
} else if (err.message.includes('HTTPS')) {
122+
errorMessage = err.message;
123+
}
124+
125+
setError(errorMessage);
83126
setState('idle');
84127
}
85128
};
@@ -109,6 +152,11 @@ export function MicButton({ onTranscript, className = '' }) {
109152
e.stopPropagation();
110153
}
111154

155+
// Don't proceed if microphone is not supported
156+
if (!isSupported) {
157+
return;
158+
}
159+
112160
// Debounce for mobile double-tap issue
113161
const now = Date.now();
114162
if (now - lastTapRef.current < 300) {
@@ -138,6 +186,14 @@ export function MicButton({ onTranscript, className = '' }) {
138186

139187
// Button appearance based on state
140188
const getButtonAppearance = () => {
189+
if (!isSupported) {
190+
return {
191+
icon: <Mic className="w-5 h-5" />,
192+
className: 'bg-gray-400 cursor-not-allowed',
193+
disabled: true
194+
};
195+
}
196+
141197
switch (state) {
142198
case 'recording':
143199
return {

src/components/QuickSettingsPanel.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ const QuickSettingsPanel = ({
142142
</label>
143143
</div>
144144

145-
{/* Whisper Dictation Settings */}
146-
<div className="space-y-2">
145+
{/* Whisper Dictation Settings - HIDDEN */}
146+
<div className="space-y-2" style={{ display: 'none' }}>
147147
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Whisper Dictation</h4>
148148

149149
<div className="space-y-2">

0 commit comments

Comments
 (0)