Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 50 additions & 14 deletions frontend/src/components/chat/ChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@
import MessageBubble from "./MessageBubble";
import SourceCard from "./SourceCard";
import {
Send,
Loader2,
Trash2,
MessageSquare,
Download,
Mic,
MicOff,
HelpCircle,
} from "lucide-react";
import { cn } from "@/lib/utils";

Send,
Loader2,
Trash2,
MessageSquare,
Download,
Mic,
MicOff,
HelpCircle,
ChevronDown,
} from "lucide-react";
import { cn } from "@/lib/utils";
interface ISpeechRecognitionEvent {
resultIndex: number;
results: {
Expand Down Expand Up @@ -96,11 +96,13 @@

// New State for Keyboard Shortcuts Help Modal
const [showHelpModal, setShowHelpModal] = useState(false);
const [showScrollButton, setShowScrollButton] = useState(false);

const recognitionRef = useRef<ISpeechRecognition | null>(null);
const initialInputRef = useRef<string>("");
const textareaRef = useRef<HTMLTextAreaElement>(null);
const bottomRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const prevDocId = useRef<string | null>(null);
const exportMenuRef = useRef<HTMLDivElement>(null);
const abortControllerRef = useRef<AbortController | null>(null);
Expand All @@ -125,9 +127,28 @@

// Auto-scroll to bottom whenever messages change
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
if (containerRef.current) {
const { scrollHeight, scrollTop, clientHeight } = containerRef.current;
if (scrollHeight - scrollTop - clientHeight < 150) {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}
} else {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}
}, [messages]);

const handleScroll = () => {
if (!containerRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
setShowScrollButton(scrollTop < scrollHeight - clientHeight - 100);
};

const scrollToBottom = () => {
if (containerRef.current) {
containerRef.current.scrollTo({ top: containerRef.current.scrollHeight, behavior: "smooth" });
}
};

useEffect(() => {
return () => {
resetChat();
Expand Down Expand Up @@ -228,7 +249,7 @@
const connectTimeout = setTimeout(() => {
try {
ws.close();
} catch (e) {

Check warning on line 252 in frontend/src/components/chat/ChatPanel.tsx

View workflow job for this annotation

GitHub Actions / βš›οΈ Frontend β€” TypeScript & Build

'e' is defined but never used
// ignore
}
reject(new Error("WebSocket connection timeout"));
Expand Down Expand Up @@ -275,12 +296,12 @@
ws.close();
resolve();
}
} catch (err) {

Check warning on line 299 in frontend/src/components/chat/ChatPanel.tsx

View workflow job for this annotation

GitHub Actions / βš›οΈ Frontend β€” TypeScript & Build

'err' is defined but never used
// ignore malformed messages
}
};

ws.onerror = (ev) => {

Check warning on line 304 in frontend/src/components/chat/ChatPanel.tsx

View workflow job for this annotation

GitHub Actions / βš›οΈ Frontend β€” TypeScript & Build

'ev' is defined but never used
clearTimeout(connectTimeout);
reject(new Error("WebSocket error"));
};
Expand All @@ -291,7 +312,7 @@
});

await wsDone;
} catch (err) {

Check warning on line 315 in frontend/src/components/chat/ChatPanel.tsx

View workflow job for this annotation

GitHub Actions / βš›οΈ Frontend β€” TypeScript & Build

'err' is defined but never used
// Fallback to existing SSE stream if WebSocket fails
try {
const stream = api.streamPost("/api/v1/chat/ask/stream", {
Expand Down Expand Up @@ -593,13 +614,15 @@
return () => {
window.removeEventListener("keydown", handleGlobalKeyDown);
};
}, [input, streaming, showHelpModal, showExportMenu, messages]); // Dependencies updated to capture fresh state data

Check warning on line 617 in frontend/src/components/chat/ChatPanel.tsx

View workflow job for this annotation

GitHub Actions / βš›οΈ Frontend β€” TypeScript & Build

React Hook useEffect has missing dependencies: 'handleClear', 'handleSend', 'setInput', 'setIsTyping', 'setStreaming', and 'toggleRecording'. Either include them or remove the dependency array

return (
<div className="h-full flex flex-col relative">
{/* ── Chat Messages ──────────────────────────── */}
<div
className="flex-1 px-4 overflow-y-auto custom-scrollbar"
<div
ref={containerRef}
onScroll={handleScroll}
className="flex-1 px-4 overflow-y-auto custom-scrollbar"
aria-busy={historyLoading}
>
{historyLoading ? (
Expand Down Expand Up @@ -674,6 +697,19 @@
<div ref={bottomRef} className="h-4" />
</div>

{/* Scroll to bottom button */}
<button
type="button"
onClick={scrollToBottom}
aria-label="Scroll to bottom"
className={cn(
"absolute right-4 bottom-20 z-50 rounded-full p-2 bg-primary text-primary-foreground shadow-lg transition-all duration-200",
showScrollButton ? "opacity-100 translate-y-0" : "opacity-0 translate-y-2 pointer-events-none"
)}
>
<ChevronDown className="h-5 w-5" />
</button>

{/* ── Input Area ─────────────────────────────── */}
<div className="border-t border-border/50 p-4 bg-card/30 backdrop-blur-sm relative">
<div className="max-w-3xl mx-auto relative">
Expand Down
Loading