From 457bcede41875d87ec86aaade289c37c9d273507 Mon Sep 17 00:00:00 2001 From: Sohika Sharma Date: Sun, 7 Jun 2026 13:20:23 +0530 Subject: [PATCH 1/5] feat(ui): improve settings modal and header interactions --- frontend/src/components/chat/ChatPanel.tsx | 60 ++++++++++----- frontend/src/components/layout/Header.tsx | 90 +++++++++++++++++----- frontend/src/lib/api.ts | 6 +- 3 files changed, 116 insertions(+), 40 deletions(-) diff --git a/frontend/src/components/chat/ChatPanel.tsx b/frontend/src/components/chat/ChatPanel.tsx index ddd47fc8..d6b88fe3 100644 --- a/frontend/src/components/chat/ChatPanel.tsx +++ b/frontend/src/components/chat/ChatPanel.tsx @@ -32,6 +32,7 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { const bottomRef = useRef(null); const prevDocId = useRef(null); const exportMenuRef = useRef(null); + const abortRef = useRef(null); useEffect(() => { const textarea = textareaRef.current; @@ -101,10 +102,16 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { cancelled = true; }; }, [activeDoc, resetChat, setMessages]); - + + const handleStop = () => { + abortRef.current?.abort(); + setStreaming(false); + setIsTyping(false); + }; const handleSend = async () => { if (!input.trim() || streaming) return; + const question = input.trim(); setInput(""); @@ -124,11 +131,17 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { setStreaming(true); setIsTyping(true); + abortRef.current = new AbortController(); + try { - const stream = api.streamPost("/api/v1/chat/ask/stream", { + const stream = api.streamPost( + "/api/v1/chat/ask/stream", + { question, document_id: activeDoc?.id || null, - }); + }, + abortRef.current.signal + ); for await (const event of stream) { if (event.type === "token") { @@ -181,21 +194,32 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { } } } catch (err) { - setIsTyping(false); - setMessages((prev) => - prev.map((m) => - m.id === assistantId - ? { - ...m, - content: t("chat.fallbackError", { - message: err instanceof Error ? err.message : "Unknown error", - }), - isStreaming: false, - } - : m - ) - ); - } finally { + if ( + err instanceof Error && + err.name === "AbortError" + ) { + return; + } + + setIsTyping(false); + + setMessages((prev) => + prev.map((m) => + m.id === assistantId + ? { + ...m, + content: t("chat.fallbackError", { + message: + err instanceof Error + ? err.message + : "Unknown error", + }), + isStreaming: false, + } + : m + ) + ); + }finally { setStreaming(false); setIsTyping(false); } diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index cf468653..68188e64 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -22,10 +22,18 @@ import { Moon, Shield, Sun, + Settings, } from "lucide-react"; -import { useSyncExternalStore } from "react"; +import { useSyncExternalStore, useState } from "react"; import { useTheme } from "next-themes"; import ApiKeyManager from "@/components/auth/ApiKeyManager"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useEffect } from "react"; interface HeaderProps { @@ -45,6 +53,18 @@ export default function Header({ sidebarOpen, onToggleSidebar, viewerOpen, onTog const router = useRouter(); const { theme, setTheme } = useTheme(); const mounted = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); // ← replaces useState + useEffect + const [settingsOpen, setSettingsOpen] = useState(false); + const [temperature, setTemperature] = useState(0.5); + useEffect(() => { + const saved = localStorage.getItem("temperature"); + if (saved) { + setTemperature(Number(saved)); + } + }, []); + + useEffect(() => { + localStorage.setItem("temperature", temperature.toString()); + }, [temperature]); const isDark = theme === "dark"; const toggleTheme = () => setTheme(isDark ? "light" : "dark"); @@ -85,27 +105,27 @@ export default function Header({ sidebarOpen, onToggleSidebar, viewerOpen, onTog {/* Right */}
- - - {mounted && ( - - )} - - + +
+ + + + + LLM Settings + + + +
+ + + + setTemperature(Number(e.target.value)) + } + className="w-full" + /> +
+
+
); } \ No newline at end of file diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index dab01f9a..08124ff1 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -280,21 +280,23 @@ class ApiClient { * Stream a POST request as Server-Sent Events. * Yields parsed SSE data objects. */ - async *streamPost(path: string, body: unknown): AsyncGenerator<{ type: string; data?: unknown }> { + async *streamPost(path: string, body: unknown, signal?: AbortSignal): AsyncGenerator<{ type: string; data?: unknown }> { let res = await this.fetchWithConnectionError(`${this.baseUrl}${path}`, { method: "POST", headers: this.getHeaders(), body: JSON.stringify(body), + signal, }); // Auto-refresh on 401 if (res.status === 401) { const newToken = await this.tryRefreshToken(); if (newToken) { - res = await this.fetchWithConnectionError(`${this.baseUrl}${path}`, { + res = await this.fetchWithConnectionError(`${this.baseUrl}${path}`, { method: "POST", headers: this.getHeaders(newToken), body: JSON.stringify(body), + signal, }); } } From a45e68a900403d0e3686403b6664638f0538cba1 Mon Sep 17 00:00:00 2001 From: Sohika Sharma Date: Wed, 10 Jun 2026 22:29:39 +0530 Subject: [PATCH 2/5] chore: trigger CI From 1c48164e3077d90f659fc1708d95142ebce63124 Mon Sep 17 00:00:00 2001 From: Sohika Sharma Date: Thu, 11 Jun 2026 13:09:51 +0530 Subject: [PATCH 3/5] fix: resolve TypeScript build errors --- frontend/package-lock.json | 1 + frontend/src/components/chat/ChatPanel.tsx | 37 +++------- frontend/src/components/layout/Header.tsx | 82 ++++------------------ 3 files changed, 22 insertions(+), 98 deletions(-) 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/components/chat/ChatPanel.tsx b/frontend/src/components/chat/ChatPanel.tsx index c5df0edb..c95cbff7 100644 --- a/frontend/src/components/chat/ChatPanel.tsx +++ b/frontend/src/components/chat/ChatPanel.tsx @@ -183,14 +183,13 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { return () => { cancelled = true; }; - }, [activeDoc, resetChat, setMessages]); + }, [activeSessionId, activeDoc, fetchSessionHistory, setMessages]); const handleStop = () => { abortRef.current?.abort(); setStreaming(false); setIsTyping(false); }; - }, [activeSessionId, activeDoc, fetchSessionHistory, setMessages]); const handleSend = async () => { if (!input.trim() || streaming) return; @@ -302,6 +301,13 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { await wsDone; } catch (err) { + if ( + err instanceof Error && + err.name === "AbortError" + ) { + return; + } + // Fallback to existing SSE stream if WebSocket fails try { const stream = api.streamPost("/api/v1/chat/ask/stream", { @@ -355,33 +361,6 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { ) ); } - } catch (err) { - if ( - err instanceof Error && - err.name === "AbortError" - ) { - return; - } - - setIsTyping(false); - - setMessages((prev) => - prev.map((m) => - m.id === assistantId - ? { - ...m, - content: t("chat.fallbackError", { - message: - err instanceof Error - ? err.message - : "Unknown error", - }), - isStreaming: false, - } - : m - ) - ); - }finally { } finally { setStreaming(false); setIsTyping(false); diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index 36af3f4e..949a810e 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState, useSyncExternalStore } from "react"; import Link from "next/link"; import { useAuth } from "@/lib/auth"; import { useRouter } from "next/navigation"; @@ -33,9 +33,9 @@ import { Briefcase, ChevronDown, Sun, + Moon, Settings, } from "lucide-react"; -import { useSyncExternalStore, useState } from "react"; import { useTheme } from "next-themes"; import ApiKeyManager from "@/components/auth/ApiKeyManager"; import { @@ -44,14 +44,8 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { useEffect } from "react"; - Moon -} from "lucide-react"; import { useWorkspaceStore, WORKSPACES, type WorkspaceId } from "@/store/workspace-store"; import { api } from "@/lib/api"; -import { useTheme } from "next-themes"; - -import { useSyncExternalStore } from "react"; interface HeaderProps { sidebarOpen: boolean; @@ -89,7 +83,6 @@ export default function Header({ useEffect(() => { localStorage.setItem("temperature", temperature.toString()); }, [temperature]); - const mounted = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); const [sheetOpen, setSheetOpen] = useState(false); const [workspaceLoading, setWorkspaceLoading] = useState(false); const workspace = useWorkspaceStore((s) => s.workspace); @@ -182,6 +175,16 @@ export default function Header({ )} + {mounted && ( - - - - - - - {user?.username?.slice(0, 2).toUpperCase() || "U"} - - - {user?.username} - - } - /> - - -
-

{user?.username}

-

{user?.email}

-
- - {user?.is_admin && ( - router.push("/admin")}> - - Admin metrics - - )} - {user?.is_admin && } - - - {t("header.signOut")} - -
-
- - + ); } From 6f784fa2e802ca00a21f698d55603e46f608c1d7 Mon Sep 17 00:00:00 2001 From: param20h Date: Fri, 12 Jun 2026 00:02:21 +0530 Subject: [PATCH 4/5] fix(ui): resolve abortRef undefined and type check errors in ChatPanel Co-authored-by: Sohika Sharma --- frontend/src/components/chat/ChatPanel.tsx | 39 ++++++++++++++++++---- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/chat/ChatPanel.tsx b/frontend/src/components/chat/ChatPanel.tsx index 1482fa17..4039e44b 100644 --- a/frontend/src/components/chat/ChatPanel.tsx +++ b/frontend/src/components/chat/ChatPanel.tsx @@ -186,7 +186,7 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { }, [activeSessionId, activeDoc, fetchSessionHistory, setMessages]); const handleStop = () => { - abortRef.current?.abort(); + abortControllerRef.current?.abort(); setStreaming(false); setIsTyping(false); }; @@ -215,8 +215,6 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { setStreaming(true); setIsTyping(true); - abortRef.current = new AbortController(); - try { // Try WebSocket first for real-time agentic thought streaming const token = typeof window !== "undefined" ? localStorage.getItem("token") : null; @@ -227,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 (e) { + // 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 })); @@ -299,11 +309,17 @@ 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.name === "AbortError" || err.message === "The user aborted a request.") ) { return; } @@ -314,7 +330,7 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { question, document_id: activeDoc?.id || null, session_id: activeSessionId, - }); + }, abortController.signal); for await (const event of stream) { if (event.type === "token") { @@ -347,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 From 782a8c44f20f6bed97dbfee97728edbd0565c5fd Mon Sep 17 00:00:00 2001 From: param20h Date: Fri, 12 Jun 2026 00:26:31 +0530 Subject: [PATCH 5/5] fix(lint): resolve eslint error and warnings across frontend --- frontend/src/app/dashboard/page.tsx | 2 +- frontend/src/app/layout.tsx | 1 - frontend/src/app/register/page.tsx | 3 - frontend/src/components/chat/ChatPanel.tsx | 21 ++++--- frontend/src/components/layout/Header.tsx | 69 +++++++++++++++++----- 5 files changed, 66 insertions(+), 30 deletions(-) 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 4039e44b..f8c8e60a 100644 --- a/frontend/src/components/chat/ChatPanel.tsx +++ b/frontend/src/components/chat/ChatPanel.tsx @@ -231,7 +231,7 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { onAbort = () => { try { ws.close(); - } catch (e) { + } catch { // ignore } reject(new DOMException("The user aborted a request.", "AbortError")); @@ -247,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")); @@ -294,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")); }; @@ -581,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(); @@ -636,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 ( @@ -844,10 +843,10 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) { + + +
{sheetOpen ? mobileSheetContent : null}
+ +