Show connection lost toast if disconnect while processing chat request

This commit is contained in:
Debanjum
2025-08-15 14:17:01 -07:00
parent 59bfaf9698
commit 25e549d683
2 changed files with 55 additions and 1 deletions

View File

@@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import "../globals.css"; import "../globals.css";
import { Toaster } from "@/components/ui/toaster";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Khoj AI - Chat", title: "Khoj AI - Chat",
@@ -39,6 +40,7 @@ export default function ChildLayout({
return ( return (
<> <>
{children} {children}
<Toaster />
<script <script
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: `window.EXCALIDRAW_ASSET_PATH = 'https://assets.khoj.dev/@excalidraw/excalidraw/dist/';`, __html: `window.EXCALIDRAW_ASSET_PATH = 'https://assets.khoj.dev/@excalidraw/excalidraw/dist/';`,

View File

@@ -33,6 +33,7 @@ import { Separator } from "@/components/ui/separator";
import { KhojLogoType } from "../components/logo/khojLogo"; import { KhojLogoType } from "../components/logo/khojLogo";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Joystick } from "@phosphor-icons/react"; import { Joystick } from "@phosphor-icons/react";
import { useToast } from "@/components/ui/use-toast";
import { ChatSidebar } from "../components/chatSidebar/chatSidebar"; import { ChatSidebar } from "../components/chatSidebar/chatSidebar";
interface ChatBodyDataProps { interface ChatBodyDataProps {
@@ -224,11 +225,17 @@ export default function Chat() {
const isMobileWidth = useIsMobileWidth(); const isMobileWidth = useIsMobileWidth();
const [isChatSideBarOpen, setIsChatSideBarOpen] = useState(false); const [isChatSideBarOpen, setIsChatSideBarOpen] = useState(false);
const [socketUrl, setSocketUrl] = useState<string | null>(null); const [socketUrl, setSocketUrl] = useState<string | null>(null);
// track whether we've already shown a toast for the current disconnect cycle to avoid duplicates
const disconnectToastShownRef = useRef(false);
// Track whether the websocket is closing due to an intentional action (page refresh/navigation or idle timeout)
const intentionalCloseRef = useRef(false);
const disconnectFromServer = useCallback(() => { const disconnectFromServer = useCallback(() => {
if (idleTimerRef.current) { if (idleTimerRef.current) {
clearTimeout(idleTimerRef.current); clearTimeout(idleTimerRef.current);
} }
// Mark as intentional so onClose does not show transient network error banner
intentionalCloseRef.current = true;
setSocketUrl(null); setSocketUrl(null);
console.log("WebSocket disconnected due to inactivity."); console.log("WebSocket disconnected due to inactivity.");
}, []); }, []);
@@ -241,6 +248,7 @@ export default function Chat() {
idleTimerRef.current = setTimeout(disconnectFromServer, idleTimeout); idleTimerRef.current = setTimeout(disconnectFromServer, idleTimeout);
}, [disconnectFromServer]); }, [disconnectFromServer]);
const { toast } = useToast();
const { sendMessage, lastMessage } = useWebSocket(socketUrl, { const { sendMessage, lastMessage } = useWebSocket(socketUrl, {
share: true, share: true,
shouldReconnect: (closeEvent) => true, shouldReconnect: (closeEvent) => true,
@@ -254,12 +262,37 @@ export default function Chat() {
onOpen: () => { onOpen: () => {
console.log("WebSocket connection established."); console.log("WebSocket connection established.");
resetIdleTimer(); resetIdleTimer();
// Reset disconnect toast guard so future disconnects can notify again
disconnectToastShownRef.current = false;
// Reset intentional close flag after a successful open
intentionalCloseRef.current = false;
}, },
onClose: () => { onClose: (event) => {
console.log("WebSocket connection closed."); console.log("WebSocket connection closed.");
if (idleTimerRef.current) { if (idleTimerRef.current) {
clearTimeout(idleTimerRef.current); clearTimeout(idleTimerRef.current);
} }
// Suppress notice if:
// - Intentional close (page refresh/navigation or idle management)
// - Normal closure (1000) or Going Away (1001 - typical on page reload)
// - No query to process
if (
!intentionalCloseRef.current &&
event?.code !== 1000 &&
event?.code !== 1001 &&
queryToProcess
) {
if (!disconnectToastShownRef.current) {
toast({
title: "Network issue",
description:
"Connection lost. Please check your network and try again when ready.",
variant: "destructive",
duration: 6000,
});
disconnectToastShownRef.current = true;
}
}
// Mark any in-progress streamed message as completed so UI updates (stop spinner, show send icon) // Mark any in-progress streamed message as completed so UI updates (stop spinner, show send icon)
setMessages((prev) => { setMessages((prev) => {
if (!prev || prev.length === 0) return prev; if (!prev || prev.length === 0) return prev;
@@ -288,9 +321,28 @@ export default function Chat() {
}); });
setProcessQuerySignal(false); setProcessQuerySignal(false);
setQueryToProcess(""); setQueryToProcess("");
if (!intentionalCloseRef.current && !disconnectToastShownRef.current) {
toast({
title: "Network error",
description:
"Connection lost. Please check your network and try again when ready.",
variant: "destructive",
duration: 5000,
});
disconnectToastShownRef.current = true;
}
}, },
}); });
// Handle page unload / refresh: mark intentional so we don't show a toast
useEffect(() => {
const handleBeforeUnload = () => {
intentionalCloseRef.current = true;
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, []);
useEffect(() => { useEffect(() => {
if (lastMessage !== null) { if (lastMessage !== null) {
resetIdleTimer(); resetIdleTimer();