From 3af9962abcaa57ae9c6d4b096d53115b7e15cf5e Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Sat, 7 Mar 2026 03:41:34 +0530 Subject: [PATCH] feat: replace image config sidebar with dialog component in ChatHeader for improved user interaction and update related state management --- .../components/new-chat/chat-header.tsx | 28 +- ...ig-sidebar.tsx => image-config-dialog.tsx} | 323 ++++++++---------- .../new-chat/model-config-dialog.tsx | 23 +- 3 files changed, 171 insertions(+), 203 deletions(-) rename surfsense_web/components/new-chat/{image-config-sidebar.tsx => image-config-dialog.tsx} (67%) diff --git a/surfsense_web/components/new-chat/chat-header.tsx b/surfsense_web/components/new-chat/chat-header.tsx index e34be791f..45a07d5a1 100644 --- a/surfsense_web/components/new-chat/chat-header.tsx +++ b/surfsense_web/components/new-chat/chat-header.tsx @@ -7,7 +7,7 @@ import type { ImageGenerationConfig, NewLLMConfigPublic, } from "@/contracts/types/new-llm-config.types"; -import { ImageConfigSidebar } from "./image-config-sidebar"; +import { ImageConfigDialog } from "./image-config-dialog"; import { ModelConfigDialog } from "./model-config-dialog"; import { ModelSelector } from "./model-selector"; @@ -25,13 +25,13 @@ export function ChatHeader({ searchSpaceId, className }: ChatHeaderProps) { const [isGlobal, setIsGlobal] = useState(false); const [dialogMode, setDialogMode] = useState<"create" | "edit" | "view">("view"); - // Image config sidebar state - const [imageSidebarOpen, setImageSidebarOpen] = useState(false); + // Image config dialog state + const [imageDialogOpen, setImageDialogOpen] = useState(false); const [selectedImageConfig, setSelectedImageConfig] = useState< ImageGenerationConfig | GlobalImageGenConfig | null >(null); const [isImageGlobal, setIsImageGlobal] = useState(false); - const [imageSidebarMode, setImageSidebarMode] = useState<"create" | "edit" | "view">("view"); + const [imageDialogMode, setImageDialogMode] = useState<"create" | "edit" | "view">("view"); // LLM handlers const handleEditLLMConfig = useCallback( @@ -60,22 +60,22 @@ export function ChatHeader({ searchSpaceId, className }: ChatHeaderProps) { const handleAddImageModel = useCallback(() => { setSelectedImageConfig(null); setIsImageGlobal(false); - setImageSidebarMode("create"); - setImageSidebarOpen(true); + setImageDialogMode("create"); + setImageDialogOpen(true); }, []); const handleEditImageConfig = useCallback( (config: ImageGenerationConfig | GlobalImageGenConfig, global: boolean) => { setSelectedImageConfig(config); setIsImageGlobal(global); - setImageSidebarMode(global ? "view" : "edit"); - setImageSidebarOpen(true); + setImageDialogMode(global ? "view" : "edit"); + setImageDialogOpen(true); }, [] ); - const handleImageSidebarClose = useCallback((open: boolean) => { - setImageSidebarOpen(open); + const handleImageDialogClose = useCallback((open: boolean) => { + setImageDialogOpen(open); if (!open) setSelectedImageConfig(null); }, []); @@ -96,13 +96,13 @@ export function ChatHeader({ searchSpaceId, className }: ChatHeaderProps) { searchSpaceId={searchSpaceId} mode={dialogMode} /> - ); diff --git a/surfsense_web/components/new-chat/image-config-sidebar.tsx b/surfsense_web/components/new-chat/image-config-dialog.tsx similarity index 67% rename from surfsense_web/components/new-chat/image-config-sidebar.tsx rename to surfsense_web/components/new-chat/image-config-dialog.tsx index 60fa60fa4..e2b8327fa 100644 --- a/surfsense_web/components/new-chat/image-config-sidebar.tsx +++ b/surfsense_web/components/new-chat/image-config-dialog.tsx @@ -1,19 +1,9 @@ "use client"; import { useAtomValue } from "jotai"; -import { - AlertCircle, - Check, - ChevronsUpDown, - Globe, - ImageIcon, - Key, - Shuffle, - X, - Zap, -} from "lucide-react"; +import { AlertCircle, Check, ChevronsUpDown, X } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { toast } from "sonner"; import { @@ -47,11 +37,12 @@ import { Spinner } from "@/components/ui/spinner"; import { IMAGE_GEN_MODELS, IMAGE_GEN_PROVIDERS } from "@/contracts/enums/image-gen-providers"; import type { GlobalImageGenConfig, + ImageGenProvider, ImageGenerationConfig, } from "@/contracts/types/new-llm-config.types"; import { cn } from "@/lib/utils"; -interface ImageConfigSidebarProps { +interface ImageConfigDialogProps { open: boolean; onOpenChange: (open: boolean) => void; config: ImageGenerationConfig | GlobalImageGenConfig | null; @@ -70,24 +61,25 @@ const INITIAL_FORM = { api_version: "", }; -export function ImageConfigSidebar({ +export function ImageConfigDialog({ open, onOpenChange, config, isGlobal, searchSpaceId, mode, -}: ImageConfigSidebarProps) { +}: ImageConfigDialogProps) { const [isSubmitting, setIsSubmitting] = useState(false); const [mounted, setMounted] = useState(false); const [formData, setFormData] = useState(INITIAL_FORM); const [modelComboboxOpen, setModelComboboxOpen] = useState(false); + const [scrollPos, setScrollPos] = useState<"top" | "middle" | "bottom">("top"); + const scrollRef = useRef(null); useEffect(() => { setMounted(true); }, []); - // Reset form when opening useEffect(() => { if (open) { if (mode === "edit" && config && !isGlobal) { @@ -103,15 +95,14 @@ export function ImageConfigSidebar({ } else if (mode === "create") { setFormData(INITIAL_FORM); } + setScrollPos("top"); } }, [open, mode, config, isGlobal]); - // Mutations const { mutateAsync: createConfig } = useAtomValue(createImageGenConfigMutationAtom); const { mutateAsync: updateConfig } = useAtomValue(updateImageGenConfigMutationAtom); const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom); - // Escape key useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape" && open) onOpenChange(false); @@ -120,6 +111,13 @@ export function ImageConfigSidebar({ return () => window.removeEventListener("keydown", handleEscape); }, [open, onOpenChange]); + const handleScroll = useCallback((e: React.UIEvent) => { + const el = e.currentTarget; + const atTop = el.scrollTop <= 2; + const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 2; + setScrollPos(atTop ? "top" : atBottom ? "bottom" : "middle"); + }, []); + const isAutoMode = config && "is_auto_mode" in config && config.is_auto_mode; const suggestedModels = useMemo(() => { @@ -134,21 +132,27 @@ export function ImageConfigSidebar({ return "Edit Image Model"; }; + const getSubtitle = () => { + if (mode === "create") return "Set up a new image generation provider"; + if (isAutoMode) return "Automatically routes requests across providers"; + if (isGlobal) return "Read-only global configuration"; + return "Update your image model settings"; + }; + const handleSubmit = useCallback(async () => { setIsSubmitting(true); try { if (mode === "create") { - const result = await createConfig({ - name: formData.name, - provider: formData.provider, - model_name: formData.model_name, - api_key: formData.api_key, + const result = await createConfig({ + name: formData.name, + provider: formData.provider as ImageGenProvider, + model_name: formData.model_name, + api_key: formData.api_key, api_base: formData.api_base || undefined, api_version: formData.api_version || undefined, description: formData.description || undefined, search_space_id: searchSpaceId, }); - // Set as active image model if (result?.id) { await updatePreferences({ search_space_id: searchSpaceId, @@ -158,14 +162,14 @@ export function ImageConfigSidebar({ toast.success("Image model created and assigned!"); onOpenChange(false); } else if (!isGlobal && config) { - await updateConfig({ - id: config.id, - data: { - name: formData.name, - description: formData.description || undefined, - provider: formData.provider, - model_name: formData.model_name, - api_key: formData.api_key, + await updateConfig({ + id: config.id, + data: { + name: formData.name, + description: formData.description || undefined, + provider: formData.provider as ImageGenProvider, + model_name: formData.model_name, + api_key: formData.api_key, api_base: formData.api_base || undefined, api_version: formData.api_version || undefined, }, @@ -214,126 +218,92 @@ export function ImageConfigSidebar({ if (!mounted) return null; - const sidebarContent = ( + const dialogContent = ( {open && ( <> - {/* Backdrop */} onOpenChange(false)} /> - {/* Sidebar */} - {/* Header */}
e.stopPropagation()} + onKeyDown={(e) => { if (e.key === "Escape") onOpenChange(false); }} > -
-
- {isAutoMode ? ( - - ) : ( - - )} -
-
-

{getTitle()}

-
- {isAutoMode ? ( - - + {/* Header */} +
+
+
+

{getTitle()}

+ {isAutoMode && ( + Recommended - ) : isGlobal ? ( - - + )} + {isGlobal && !isAutoMode && mode !== "create" && ( + Global - ) : null} - {config && !isAutoMode && ( - {config.model_name} )}
+

{getSubtitle()}

+ {config && !isAutoMode && mode !== "create" && ( +

{config.model_name}

+ )}
+
- -
- {/* Content */} -
-
- {/* Auto mode */} + {/* Scrollable content */} +
{isAutoMode && ( - <> - - - - Auto mode distributes image generation requests across all configured - providers for optimal performance and rate limit protection. - - -
- - -
- + + + Auto mode distributes image generation requests across all configured + providers for optimal performance and rate limit protection. + + )} - {/* Global config (read-only) */} {isGlobal && !isAutoMode && config && ( <> - + Global configurations are read-only. To customize, create a new model. @@ -372,29 +342,11 @@ export function ImageConfigSidebar({
-
- - -
)} - {/* Create / Edit form */} {(mode === "create" || (mode === "edit" && !isGlobal)) && (
- {/* Name */}
- {/* Description */}
- {/* Provider */}
- {/* Model Name */}
{suggestedModels.length > 0 ? ( @@ -452,14 +398,14 @@ export function ImageConfigSidebar({ - - + + - {/* API Key */}
- +
- {/* API Base */}
- {/* Azure API Version */} {formData.provider === "AZURE_OPENAI" && (
@@ -549,28 +490,54 @@ export function ImageConfigSidebar({ />
)} - - {/* Actions */} -
- - -
)}
+ + {/* Fixed footer */} +
+ + {(mode === "create" || (mode === "edit" && !isGlobal)) ? ( + + ) : isAutoMode ? ( + + ) : isGlobal && config ? ( + + ) : null} +
@@ -578,5 +545,5 @@ export function ImageConfigSidebar({ ); - return typeof document !== "undefined" ? createPortal(sidebarContent, document.body) : null; + return typeof document !== "undefined" ? createPortal(dialogContent, document.body) : null; } diff --git a/surfsense_web/components/new-chat/model-config-dialog.tsx b/surfsense_web/components/new-chat/model-config-dialog.tsx index 0d58786be..6b91290c1 100644 --- a/surfsense_web/components/new-chat/model-config-dialog.tsx +++ b/surfsense_web/components/new-chat/model-config-dialog.tsx @@ -18,6 +18,7 @@ import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; import type { GlobalNewLLMConfig, + LiteLLMProvider, NewLLMConfigPublic, } from "@/contracts/types/new-llm-config.types"; import { cn } from "@/lib/utils"; @@ -408,10 +409,10 @@ export function ModelConfigDialog({ initialData={{ name: config.name, description: config.description, - provider: config.provider, + provider: config.provider as LiteLLMProvider, custom_provider: config.custom_provider, model_name: config.model_name, - api_key: config.api_key, + api_key: "api_key" in config ? (config.api_key as string) : "", api_base: config.api_base, litellm_params: config.litellm_params, system_instructions: config.system_instructions, @@ -430,14 +431,14 @@ export function ModelConfigDialog({ {/* Fixed footer */}
- {(mode === "create" || (!isGlobal && !isAutoMode && config)) ? (