diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index 14f4aeafb..dfcda2d9f 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -43,6 +43,7 @@ import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import { AssistantMessage } from "@/components/assistant-ui/assistant-message"; import { ChatSessionStatus } from "@/components/assistant-ui/chat-session-status"; import { ConnectorIndicator } from "@/components/assistant-ui/connector-popup"; +import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup"; import { InlineMentionEditor, type InlineMentionEditorRef, @@ -478,6 +479,7 @@ const ComposerAction: FC = ({ const mentionedDocuments = useAtomValue(mentionedDocumentsAtom); const sidebarDocs = useAtomValue(sidebarSelectedDocumentsAtom); const setDocumentsSidebarOpen = useSetAtom(documentsSidebarOpenAtom); + const { openDialog: openUploadDialog } = useDocumentUploadDialog(); const isComposerTextEmpty = useAssistantState(({ composer }) => { const text = composer.text?.trim() || ""; @@ -514,8 +516,8 @@ const ComposerAction: FC = ({ variant="ghost" size="icon" className="size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30" - aria-label="Open documents" - onClick={() => setDocumentsSidebarOpen(true)} + aria-label="Upload documents" + onClick={openUploadDialog} > diff --git a/surfsense_web/components/inference-params-editor.tsx b/surfsense_web/components/inference-params-editor.tsx index b29275611..3764b1dac 100644 --- a/surfsense_web/components/inference-params-editor.tsx +++ b/surfsense_web/components/inference-params-editor.tsx @@ -1,6 +1,6 @@ "use client"; -import { Plus, Trash2 } from "lucide-react"; +import { Trash2 } from "lucide-react"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -76,7 +76,7 @@ export default function InferenceParamsEditor({ params, setParams }: InferencePa - + {PARAM_KEYS.map((key) => ( {key} @@ -104,7 +104,7 @@ export default function InferenceParamsEditor({ params, setParams }: InferencePa onClick={handleAdd} disabled={!selectedKey || value === ""} > - Add Parameter + Add Parameter diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarHeader.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarHeader.tsx index bab3c6917..d61a0f205 100644 --- a/surfsense_web/components/layout/ui/sidebar/SidebarHeader.tsx +++ b/surfsense_web/components/layout/ui/sidebar/SidebarHeader.tsx @@ -8,7 +8,6 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { cn } from "@/lib/utils"; @@ -56,7 +55,6 @@ export function SidebarHeader({ {t("manage_members")} - {t("search_space_settings")} diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx index aa38910b5..6eeeed5e9 100644 --- a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx +++ b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx @@ -188,7 +188,7 @@ export function SidebarUserProfile({ - + @@ -256,7 +256,7 @@ export function SidebarUserProfile({ - + {isLoggingOut ? ( @@ -310,7 +310,7 @@ export function SidebarUserProfile({ - + @@ -378,7 +378,7 @@ export function SidebarUserProfile({ - + {isLoggingOut ? : } diff --git a/surfsense_web/components/new-chat/chat-header.tsx b/surfsense_web/components/new-chat/chat-header.tsx index 4d40bc21a..e34be791f 100644 --- a/surfsense_web/components/new-chat/chat-header.tsx +++ b/surfsense_web/components/new-chat/chat-header.tsx @@ -8,7 +8,7 @@ import type { NewLLMConfigPublic, } from "@/contracts/types/new-llm-config.types"; import { ImageConfigSidebar } from "./image-config-sidebar"; -import { ModelConfigSidebar } from "./model-config-sidebar"; +import { ModelConfigDialog } from "./model-config-dialog"; import { ModelSelector } from "./model-selector"; interface ChatHeaderProps { @@ -17,13 +17,13 @@ interface ChatHeaderProps { } export function ChatHeader({ searchSpaceId, className }: ChatHeaderProps) { - // LLM config sidebar state - const [sidebarOpen, setSidebarOpen] = useState(false); + // LLM config dialog state + const [dialogOpen, setDialogOpen] = useState(false); const [selectedConfig, setSelectedConfig] = useState< NewLLMConfigPublic | GlobalNewLLMConfig | null >(null); const [isGlobal, setIsGlobal] = useState(false); - const [sidebarMode, setSidebarMode] = useState<"create" | "edit" | "view">("view"); + const [dialogMode, setDialogMode] = useState<"create" | "edit" | "view">("view"); // Image config sidebar state const [imageSidebarOpen, setImageSidebarOpen] = useState(false); @@ -38,8 +38,8 @@ export function ChatHeader({ searchSpaceId, className }: ChatHeaderProps) { (config: NewLLMConfigPublic | GlobalNewLLMConfig, global: boolean) => { setSelectedConfig(config); setIsGlobal(global); - setSidebarMode(global ? "view" : "edit"); - setSidebarOpen(true); + setDialogMode(global ? "view" : "edit"); + setDialogOpen(true); }, [] ); @@ -47,12 +47,12 @@ export function ChatHeader({ searchSpaceId, className }: ChatHeaderProps) { const handleAddNewLLM = useCallback(() => { setSelectedConfig(null); setIsGlobal(false); - setSidebarMode("create"); - setSidebarOpen(true); + setDialogMode("create"); + setDialogOpen(true); }, []); - const handleSidebarClose = useCallback((open: boolean) => { - setSidebarOpen(open); + const handleDialogClose = useCallback((open: boolean) => { + setDialogOpen(open); if (!open) setSelectedConfig(null); }, []); @@ -88,13 +88,13 @@ export function ChatHeader({ searchSpaceId, className }: ChatHeaderProps) { onAddNewImage={handleAddImageModel} className={className} /> - void; config: NewLLMConfigPublic | GlobalNewLLMConfig | null; @@ -30,28 +31,34 @@ interface ModelConfigSidebarProps { mode: "create" | "edit" | "view"; } -export function ModelConfigSidebar({ +export function ModelConfigDialog({ open, onOpenChange, config, isGlobal, searchSpaceId, mode, -}: ModelConfigSidebarProps) { +}: ModelConfigDialogProps) { const [isSubmitting, setIsSubmitting] = useState(false); const [mounted, setMounted] = useState(false); + const [scrollPos, setScrollPos] = useState<"top" | "middle" | "bottom">("top"); + const scrollRef = useRef(null); - // Handle SSR - only render portal on client useEffect(() => { setMounted(true); }, []); - // Mutations - use mutateAsync from the atom value + 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 { mutateAsync: createConfig } = useAtomValue(createNewLLMConfigMutationAtom); const { mutateAsync: updateConfig } = useAtomValue(updateNewLLMConfigMutationAtom); const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom); - // Handle escape key useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape" && open) { @@ -62,10 +69,8 @@ export function ModelConfigSidebar({ return () => window.removeEventListener("keydown", handleEscape); }, [open, onOpenChange]); - // Check if this is Auto mode const isAutoMode = config && "is_auto_mode" in config && config.is_auto_mode; - // Get title based on mode const getTitle = () => { if (mode === "create") return "Add New Configuration"; if (isAutoMode) return "Auto Mode (Fastest)"; @@ -73,19 +78,23 @@ export function ModelConfigSidebar({ return "Edit Configuration"; }; - // Handle form submit + const getSubtitle = () => { + if (mode === "create") return "Set up a new LLM provider for this search space"; + if (isAutoMode) return "Automatically routes requests across providers"; + if (isGlobal) return "Read-only global configuration"; + return "Update your configuration settings"; + }; + const handleSubmit = useCallback( async (data: LLMConfigFormData) => { setIsSubmitting(true); try { if (mode === "create") { - // Create new config const result = await createConfig({ ...data, search_space_id: searchSpaceId, }); - // Assign the new config to the agent role if (result?.id) { await updatePreferences({ search_space_id: searchSpaceId, @@ -98,7 +107,6 @@ export function ModelConfigSidebar({ toast.success("Configuration created and assigned!"); onOpenChange(false); } else if (!isGlobal && config) { - // Update existing user config await updateConfig({ id: config.id, data: { @@ -137,7 +145,6 @@ export function ModelConfigSidebar({ ] ); - // Handle "Use this model" for global configs const handleUseGlobalConfig = useCallback(async () => { if (!config || !isGlobal) return; setIsSubmitting(true); @@ -160,7 +167,7 @@ export function ModelConfigSidebar({ if (!mounted) return null; - const sidebarContent = ( + const dialogContent = ( {open && ( <> @@ -169,93 +176,80 @@ export function ModelConfigSidebar({ initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} - transition={{ duration: 0.2 }} - className="fixed inset-0 z-[24] bg-black/20 backdrop-blur-sm" + transition={{ duration: 0.15 }} + className="fixed inset-0 z-[24] bg-black/50 backdrop-blur-sm" onClick={() => onOpenChange(false)} /> - {/* Sidebar Panel */} + {/* Dialog */} - {/* Header */}
e.stopPropagation()} + onKeyDown={(e) => { if (e.key === "Escape") onOpenChange(false); }} > -
-
- {isAutoMode ? ( - - ) : ( - - )} -
-
-

{getTitle()}

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

{getTitle()}

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

{getSubtitle()}

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

{config.model_name}

+ )}
+
- -
- {/* Content - use overflow-y-auto instead of ScrollArea for better compatibility */} -
-
- {/* Auto mode info banner */} + {/* Scrollable content */} +
{isAutoMode && ( - - + Auto mode automatically distributes requests across all available LLM providers to optimize performance and avoid rate limits. @@ -263,9 +257,8 @@ export function ModelConfigSidebar({ )} - {/* Global config notice */} {isGlobal && !isAutoMode && mode !== "create" && ( - + Global configurations are read-only. To customize settings, create a new @@ -274,20 +267,17 @@ export function ModelConfigSidebar({ )} - {/* Form */} {mode === "create" ? ( onOpenChange(false)} isSubmitting={isSubmitting} mode="create" - submitLabel="Create & Use" + formId="model-config-form" + hideActions /> ) : isAutoMode && config ? ( - // Special view for Auto mode
- {/* Auto Mode Features */}
@@ -340,35 +330,9 @@ export function ModelConfigSidebar({
- {/* Action Buttons */} -
- - -
) : isGlobal && config ? ( - // Read-only view for global configs
- {/* Config Details */}
@@ -437,33 +401,8 @@ export function ModelConfigSidebar({ )}
- {/* Action Buttons */} -
- - -
) : config ? ( - // Edit form for user configs onOpenChange(false)} isSubmitting={isSubmitting} mode="edit" - submitLabel="Save Changes" + formId="model-config-form" + hideActions /> ) : null}
+ + {/* Fixed footer */} +
+ + {(mode === "create" || (!isGlobal && !isAutoMode && config)) ? ( + + ) : isAutoMode ? ( + + ) : isGlobal && config ? ( + + ) : null} +
@@ -495,5 +480,5 @@ export function ModelConfigSidebar({ ); - return typeof document !== "undefined" ? createPortal(sidebarContent, document.body) : null; + return typeof document !== "undefined" ? createPortal(dialogContent, document.body) : null; } diff --git a/surfsense_web/components/shared/llm-config-form.tsx b/surfsense_web/components/shared/llm-config-form.tsx index 648457919..0d3e67755 100644 --- a/surfsense_web/components/shared/llm-config-form.tsx +++ b/surfsense_web/components/shared/llm-config-form.tsx @@ -2,16 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useAtomValue } from "jotai"; -import { - Bot, - Check, - ChevronDown, - ChevronsUpDown, - Key, - MessageSquareQuote, - Rocket, - Sparkles, -} from "lucide-react"; +import { Check, ChevronDown, ChevronsUpDown } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import { useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; @@ -88,6 +79,8 @@ interface LLMConfigFormProps { submitLabel?: string; showAdvanced?: boolean; compact?: boolean; + formId?: string; + hideActions?: boolean; } export function LLMConfigForm({ @@ -100,6 +93,8 @@ export function LLMConfigForm({ submitLabel, showAdvanced = true, compact = false, + formId, + hideActions = false, }: LLMConfigFormProps) { const { data: defaultInstructions, isSuccess: defaultInstructionsLoaded } = useAtomValue( defaultSystemInstructionsAtom @@ -164,11 +159,10 @@ export function LLMConfigForm({ return (
- + {/* Model Configuration Section */}
-
- +
Model Configuration
@@ -179,14 +173,12 @@ export function LLMConfigForm({ name="name" render={({ field }) => ( - - + Configuration Name @@ -224,19 +216,18 @@ export function LLMConfigForm({ LLM Provider