diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/circleback-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/circleback-config.tsx index 26c070b0b..2fcc5e131 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/circleback-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/circleback-config.tsx @@ -3,6 +3,7 @@ import { Copy, Webhook, Check, Info } from "lucide-react"; import { useState, useEffect } from "react"; import type { FC } from "react"; +import { z } from "zod"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; @@ -14,13 +15,25 @@ export interface CirclebackConfigProps extends ConnectorConfigProps { onNameChange?: (name: string) => void; } +// Type-safe schema for webhook info response +const circlebackWebhookInfoSchema = z.object({ + webhook_url: z.string(), + search_space_id: z.number(), + method: z.string(), + content_type: z.string(), + description: z.string(), + note: z.string(), +}); + +export type CirclebackWebhookInfo = z.infer; + export const CirclebackConfig: FC = ({ connector, onNameChange, }) => { const [name, setName] = useState(connector.name || ""); const [webhookUrl, setWebhookUrl] = useState(""); - const [webhookInfo, setWebhookInfo] = useState<{ webhook_url: string; search_space_id: number; method: string; content_type: string; description: string; note: string } | null>(null); + const [webhookInfo, setWebhookInfo] = useState(null); const [isLoading, setIsLoading] = useState(true); const [copied, setCopied] = useState(false); @@ -34,18 +47,30 @@ export const CirclebackConfig: FC = ({ const fetchWebhookInfo = async () => { if (!connector.search_space_id) return; + const baseUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL; + if (!baseUrl) { + console.error("NEXT_PUBLIC_FASTAPI_BACKEND_URL is not configured"); + setIsLoading(false); + return; + } + setIsLoading(true); try { const response = await authenticatedFetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/webhooks/circleback/${connector.search_space_id}/info` + `${baseUrl}/api/v1/webhooks/circleback/${connector.search_space_id}/info` ); if (response.ok) { - const data = await response.json(); - setWebhookInfo(data); - setWebhookUrl(data.webhook_url || ""); + const data: unknown = await response.json(); + // Runtime validation with zod schema + const validatedData = circlebackWebhookInfoSchema.parse(data); + setWebhookInfo(validatedData); + setWebhookUrl(validatedData.webhook_url); } } catch (error) { console.error("Failed to fetch webhook info:", error); + // Reset state on error + setWebhookInfo(null); + setWebhookUrl(""); } finally { setIsLoading(false); } diff --git a/surfsense_web/components/sidebar/app-sidebar.tsx b/surfsense_web/components/sidebar/app-sidebar.tsx index 08291c392..6afe2dacb 100644 --- a/surfsense_web/components/sidebar/app-sidebar.tsx +++ b/surfsense_web/components/sidebar/app-sidebar.tsx @@ -295,7 +295,6 @@ export const AppSidebar = memo(function AppSidebar({ const { theme, setTheme } = useTheme(); const { data: user, isPending: isLoadingUser } = useAtomValue(currentUserAtom); const [isClient, setIsClient] = useState(false); - const [isSourcesExpanded, setIsSourcesExpanded] = useState(false); useEffect(() => { setIsClient(true); @@ -447,19 +446,17 @@ export const AppSidebar = memo(function AppSidebar({ - + diff --git a/surfsense_web/components/sidebar/nav-chats.tsx b/surfsense_web/components/sidebar/nav-chats.tsx index 5b73b75e9..4b594402b 100644 --- a/surfsense_web/components/sidebar/nav-chats.tsx +++ b/surfsense_web/components/sidebar/nav-chats.tsx @@ -12,7 +12,7 @@ import { } from "lucide-react"; import { usePathname, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useState } from "react"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { @@ -30,7 +30,6 @@ import { SidebarMenuItem, useSidebar, } from "@/components/ui/sidebar"; -import { useIsMobile } from "@/hooks/use-mobile"; import { cn } from "@/lib/utils"; import { AllChatsSidebar } from "./all-chats-sidebar"; @@ -53,7 +52,6 @@ interface NavChatsProps { chats: ChatItem[]; defaultOpen?: boolean; searchSpaceId?: string; - isSourcesExpanded?: boolean; } // Map of icon names to their components @@ -68,24 +66,15 @@ export function NavChats({ chats, defaultOpen = true, searchSpaceId, - isSourcesExpanded = false, }: NavChatsProps) { const t = useTranslations("sidebar"); const router = useRouter(); const pathname = usePathname(); - const isMobile = useIsMobile(); const { setOpenMobile } = useSidebar(); const [isDeleting, setIsDeleting] = useState(null); const [isOpen, setIsOpen] = useState(defaultOpen); const [isAllChatsSidebarOpen, setIsAllChatsSidebarOpen] = useState(false); - // Auto-collapse on smaller screens when Sources is expanded - useEffect(() => { - if (isSourcesExpanded && isMobile) { - setIsOpen(false); - } - }, [isSourcesExpanded, isMobile]); - // Handle chat deletion with loading state const handleDeleteChat = useCallback(async (chatId: number, deleteAction: () => void) => { setIsDeleting(chatId); diff --git a/surfsense_web/components/sidebar/nav-main.tsx b/surfsense_web/components/sidebar/nav-main.tsx index 404e9f083..43c551875 100644 --- a/surfsense_web/components/sidebar/nav-main.tsx +++ b/surfsense_web/components/sidebar/nav-main.tsx @@ -31,10 +31,9 @@ interface NavItem { interface NavMainProps { items: NavItem[]; - onSourcesExpandedChange?: (expanded: boolean) => void; } -export function NavMain({ items, onSourcesExpandedChange }: NavMainProps) { +export function NavMain({ items }: NavMainProps) { const t = useTranslations("nav_menu"); const pathname = usePathname(); @@ -100,16 +99,9 @@ export function NavMain({ items, onSourcesExpandedChange }: NavMainProps) { }); // Handle collapsible state change - const handleOpenChange = useCallback( - (title: string, isOpen: boolean) => { - setExpandedItems((prev) => ({ ...prev, [title]: isOpen })); - // Notify parent when Sources is expanded/collapsed - if (title === "Sources" && onSourcesExpandedChange) { - onSourcesExpandedChange(isOpen); - } - }, - [onSourcesExpandedChange] - ); + const handleOpenChange = useCallback((title: string, isOpen: boolean) => { + setExpandedItems((prev) => ({ ...prev, [title]: isOpen })); + }, []); return ( diff --git a/surfsense_web/components/sidebar/nav-notes.tsx b/surfsense_web/components/sidebar/nav-notes.tsx index c7c4073c2..ab1f3ff33 100644 --- a/surfsense_web/components/sidebar/nav-notes.tsx +++ b/surfsense_web/components/sidebar/nav-notes.tsx @@ -12,7 +12,7 @@ import { } from "lucide-react"; import { usePathname, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { @@ -31,7 +31,6 @@ import { useSidebar, } from "@/components/ui/sidebar"; import { useLogsSummary } from "@/hooks/use-logs"; -import { useIsMobile } from "@/hooks/use-mobile"; import { cn } from "@/lib/utils"; import { AllNotesSidebar } from "./all-notes-sidebar"; @@ -55,7 +54,6 @@ interface NavNotesProps { onAddNote?: () => void; defaultOpen?: boolean; searchSpaceId?: string; - isSourcesExpanded?: boolean; } // Map of icon names to their components @@ -70,12 +68,10 @@ export function NavNotes({ onAddNote, defaultOpen = true, searchSpaceId, - isSourcesExpanded = false, }: NavNotesProps) { const t = useTranslations("sidebar"); const router = useRouter(); const pathname = usePathname(); - const isMobile = useIsMobile(); const { setOpenMobile } = useSidebar(); const [isDeleting, setIsDeleting] = useState(null); const [isOpen, setIsOpen] = useState(defaultOpen); @@ -98,13 +94,6 @@ export function NavNotes({ ); }, [summary?.active_tasks]); - // Auto-collapse on smaller screens when Sources is expanded - useEffect(() => { - if (isSourcesExpanded && isMobile) { - setIsOpen(false); - } - }, [isSourcesExpanded, isMobile]); - // Handle note deletion with loading state const handleDeleteNote = useCallback(async (noteId: number, deleteAction: () => void) => { setIsDeleting(noteId);