diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ac9effab..41685711 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -6631,6 +6631,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index 50183f5d..8bf9be67 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -57,7 +57,7 @@ export interface DocInfo { } export default function DashboardPage() { - const { user, loading, initialized } = useAuth(); + const { user, initialized } = useAuth(); const router = useRouter(); const [documents, setDocuments] = useState([]); diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 456ce685..cb87453f 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -5,7 +5,6 @@ import { AuthProvider } from "@/lib/auth"; import { TooltipProvider } from "@/components/ui/tooltip"; import I18nProvider from "@/components/providers/I18nProvider"; import { ThemeProvider } from "@/components/layout/ThemeProvider"; -import { Toaster } from "sonner"; const inter = Inter({ variable: "--font-sans", diff --git a/frontend/src/app/register/page.tsx b/frontend/src/app/register/page.tsx index f74e7dd1..c81ae20e 100644 --- a/frontend/src/app/register/page.tsx +++ b/frontend/src/app/register/page.tsx @@ -11,7 +11,6 @@ import { Brain } from "lucide-react"; import Link from "next/link"; import GoogleSignInButton from "@/components/auth/GoogleSignInButton"; import { PasswordField } from "@/components/auth/PasswordField"; -import { isPasswordValid } from "@/lib/password-validation"; import HuggingFaceSignInButton from "@/components/auth/HuggingFaceSignInButton"; export default function RegisterPage() { @@ -27,8 +26,6 @@ export default function RegisterPage() { const [verificationUrl, setVerificationUrl] = useState(""); const [loading, setLoading] = useState(false); - const passwordValid = isPasswordValid(password); - const canSubmit = username.trim().length >= 3 && email.trim().length > 0 && passwordValid && !loading; // Redirect if already logged in useEffect(() => { if (initialized && user) { diff --git a/frontend/src/components/chat/ChatPanel.tsx b/frontend/src/components/chat/ChatPanel.tsx index 362d9844..f8c8e60a 100644 --- a/frontend/src/components/chat/ChatPanel.tsx +++ b/frontend/src/components/chat/ChatPanel.tsx @@ -184,10 +184,17 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { cancelled = true; }; }, [activeSessionId, activeDoc, fetchSessionHistory, setMessages]); + + const handleStop = () => { + abortControllerRef.current?.abort(); + setStreaming(false); + setIsTyping(false); + }; const handleSend = async () => { if (!input.trim() || streaming) return; + const question = input.trim(); setInput(""); @@ -218,7 +225,19 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { const ws = new WebSocket(wsUrl); + let onAbort: (() => void) | null = null; + const wsDone = new Promise((resolve, reject) => { + onAbort = () => { + try { + ws.close(); + } catch { + // ignore + } + reject(new DOMException("The user aborted a request.", "AbortError")); + }; + abortController.signal.addEventListener("abort", onAbort); + ws.onopen = () => { // Send initial payload ws.send(JSON.stringify({ question, document_id: activeDoc?.id || null, session_id: activeSessionId })); @@ -228,7 +247,7 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { const connectTimeout = setTimeout(() => { try { ws.close(); - } catch (e) { + } catch { // ignore } reject(new Error("WebSocket connection timeout")); @@ -275,12 +294,12 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { ws.close(); resolve(); } - } catch (err) { + } catch { // ignore malformed messages } }; - ws.onerror = (ev) => { + ws.onerror = () => { clearTimeout(connectTimeout); reject(new Error("WebSocket error")); }; @@ -290,15 +309,28 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { }; }); - await wsDone; + try { + await wsDone; + } finally { + if (onAbort) { + abortController.signal.removeEventListener("abort", onAbort); + } + } } catch (err) { + if ( + err instanceof Error && + (err.name === "AbortError" || err.message === "The user aborted a request.") + ) { + return; + } + // Fallback to existing SSE stream if WebSocket fails try { const stream = api.streamPost("/api/v1/chat/ask/stream", { question, document_id: activeDoc?.id || null, session_id: activeSessionId, - }); + }, abortController.signal); for await (const event of stream) { if (event.type === "token") { @@ -331,6 +363,17 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { } } catch (err2) { setIsTyping(false); + if ( + err2 instanceof Error && + (err2.name === "AbortError" || err2.message === "The user aborted a request.") + ) { + setMessages((prev) => + prev.map((m) => + m.id === assistantId ? { ...m, isStreaming: false } : m + ) + ); + return; + } setMessages((prev) => prev.map((m) => m.id === assistantId @@ -538,11 +581,9 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { // Shortcut 2: Escape → Abort SSE stream OR clear input OR close modal if (e.key === "Escape") { - if (streaming && abortControllerRef.current) { + if (streaming) { e.preventDefault(); - abortControllerRef.current.abort(); - setStreaming(false); - setIsTyping(false); + handleStop(); toast.info("Response cancelled"); } else if (document.activeElement === textareaRef.current) { e.preventDefault(); @@ -593,6 +634,7 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { return () => { window.removeEventListener("keydown", handleGlobalKeyDown); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [input, streaming, showHelpModal, showExportMenu, messages]); // Dependencies updated to capture fresh state data return ( @@ -801,10 +843,10 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { + {mounted && (