diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx index 8964ef3d6..4800491f8 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx @@ -137,19 +137,19 @@ export function DocumentsTableShell({
-
-

{t("no_documents")}

-

- Get started by uploading your first document. -

-
- +
+

{t("no_documents")}

+

+ Get started by uploading your first document. +

+
+ ) : ( diff --git a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx index eb7bc23fc..239fdc5c1 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx @@ -517,4 +517,4 @@ export default function EditorPage() { ); -} \ No newline at end of file +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx index ae3c5ad9c..1631f00b9 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx @@ -26,13 +26,13 @@ export default function DashboardLayout({ }, ]; - const customNavMain = [ - { - title: "Chat", - url: `/dashboard/${search_space_id}/new-chat`, - icon: "MessageCircle", - items: [], - }, + const customNavMain = [ + { + title: "Chat", + url: `/dashboard/${search_space_id}/new-chat`, + icon: "MessageCircle", + items: [], + }, { title: "Documents", url: `/dashboard/${search_space_id}/documents`, diff --git a/surfsense_web/components/assistant-ui/assistant-message.tsx b/surfsense_web/components/assistant-ui/assistant-message.tsx index 62fbe0dd4..0fd70800a 100644 --- a/surfsense_web/components/assistant-ui/assistant-message.tsx +++ b/surfsense_web/components/assistant-ui/assistant-message.tsx @@ -9,7 +9,10 @@ import { CheckIcon, CopyIcon, DownloadIcon, RefreshCwIcon } from "lucide-react"; import type { FC } from "react"; import { useContext } from "react"; import { MarkdownText } from "@/components/assistant-ui/markdown-text"; -import { ThinkingStepsContext, ThinkingStepsDisplay } from "@/components/assistant-ui/thinking-steps"; +import { + ThinkingStepsContext, + ThinkingStepsDisplay, +} from "@/components/assistant-ui/thinking-steps"; import { ToolFallback } from "@/components/assistant-ui/tool-fallback"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { BranchPicker } from "@/components/assistant-ui/branch-picker"; @@ -115,4 +118,3 @@ const AssistantActionBar: FC = () => { ); }; - diff --git a/surfsense_web/components/assistant-ui/branch-picker.tsx b/surfsense_web/components/assistant-ui/branch-picker.tsx index 1d9041309..ee4addd2a 100644 --- a/surfsense_web/components/assistant-ui/branch-picker.tsx +++ b/surfsense_web/components/assistant-ui/branch-picker.tsx @@ -30,4 +30,3 @@ export const BranchPicker: FC = ({ className, ); }; - diff --git a/surfsense_web/components/assistant-ui/composer-action.tsx b/surfsense_web/components/assistant-ui/composer-action.tsx index d359342d1..8d18ae2a9 100644 --- a/surfsense_web/components/assistant-ui/composer-action.tsx +++ b/surfsense_web/components/assistant-ui/composer-action.tsx @@ -74,7 +74,9 @@ const ConnectorIndicator: FC = () => { "text-muted-foreground" )} aria-label={ - hasConnectors ? `View ${activeConnectorsCount} active connectors` : "Add your first connector" + hasConnectors + ? `View ${activeConnectorsCount} active connectors` + : "Add your first connector" } onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} @@ -137,7 +139,9 @@ const ConnectorIndicator: FC = () => { className="flex items-center gap-1.5 rounded-md bg-muted/80 px-2.5 py-1.5 text-xs border border-border/50" > {getConnectorIcon(docType, "size-3.5")} - {getDocumentTypeLabel(docType)} + + {getDocumentTypeLabel(docType)} + {count > 999 ? "999+" : count} @@ -150,7 +154,9 @@ const ConnectorIndicator: FC = () => { - ); }; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx index 843c91187..d6c1faf98 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx @@ -63,12 +63,13 @@ export const ConnectorEditView: FC = ({ const checkScrollState = useCallback(() => { if (!scrollContainerRef.current) return; - + const target = scrollContainerRef.current; const scrolled = target.scrollTop > 0; - const hasMore = target.scrollHeight > target.clientHeight && + const hasMore = + target.scrollHeight > target.clientHeight && target.scrollTop + target.clientHeight < target.scrollHeight - 10; - + setIsScrolled(scrolled); setHasMoreContent(hasMore); }, []); @@ -83,11 +84,11 @@ export const ConnectorEditView: FC = ({ const resizeObserver = new ResizeObserver(() => { checkScrollState(); }); - + if (scrollContainerRef.current) { resizeObserver.observe(scrollContainerRef.current); } - + return () => { resizeObserver.disconnect(); }; @@ -109,10 +110,12 @@ export const ConnectorEditView: FC = ({ return (
{/* Fixed Header */} -
+
{/* Back button */}
-

- {connector.name} -

+

{connector.name}

Manage your connector settings and sync configuration

{/* Quick Index Button - only show for indexable connectors, but not for Google Drive (requires folder selection) */} - {connector.is_indexable && onQuickIndex && connector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && ( - - )} + {connector.is_indexable && + onQuickIndex && + connector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && ( + + )}
{/* Scrollable Content */}
-
@@ -184,14 +187,15 @@ export const ConnectorEditView: FC = ({ {connector.is_indexable && ( <> {/* Date range selector - not shown for Google Drive (uses folder selection) or Webcrawler (uses config) */} - {connector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && connector.connector_type !== "WEBCRAWLER_CONNECTOR" && ( - - )} + {connector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && + connector.connector_type !== "WEBCRAWLER_CONNECTOR" && ( + + )} {/* Periodic sync - not shown for Google Drive */} {connector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && ( @@ -212,9 +216,12 @@ export const ConnectorEditView: FC = ({
-

Re-indexing runs in the background

+

+ Re-indexing runs in the background +

- You can continue using SurfSense while we sync your data. Check the Active tab to see progress. + You can continue using SurfSense while we sync your data. Check the Active tab + to see progress.

@@ -235,7 +242,9 @@ export const ConnectorEditView: FC = ({
{showDisconnectConfirm ? (
- Are you sure? + + Are you sure? +
)} -
); }; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx index 716e1c201..e8ffde2cf 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/indexing-configuration-view.tsx @@ -45,7 +45,7 @@ export const IndexingConfigurationView: FC = ({ }) => { // Get connector-specific config component const ConnectorConfigComponent = useMemo( - () => connector ? getConnectorConfigComponent(connector.connector_type) : null, + () => (connector ? getConnectorConfigComponent(connector.connector_type) : null), [connector] ); const [isScrolled, setIsScrolled] = useState(false); @@ -54,12 +54,13 @@ export const IndexingConfigurationView: FC = ({ const checkScrollState = useCallback(() => { if (!scrollContainerRef.current) return; - + const target = scrollContainerRef.current; const scrolled = target.scrollTop > 0; - const hasMore = target.scrollHeight > target.clientHeight && + const hasMore = + target.scrollHeight > target.clientHeight && target.scrollTop + target.clientHeight < target.scrollHeight - 10; - + setIsScrolled(scrolled); setHasMoreContent(hasMore); }, []); @@ -74,11 +75,11 @@ export const IndexingConfigurationView: FC = ({ const resizeObserver = new ResizeObserver(() => { checkScrollState(); }); - + if (scrollContainerRef.current) { resizeObserver.observe(scrollContainerRef.current); } - + return () => { resizeObserver.disconnect(); }; @@ -87,10 +88,12 @@ export const IndexingConfigurationView: FC = ({ return (
{/* Fixed Header */} -
+
{/* Back button */} -
); }; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts index 46f6cb130..06860fb8f 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts @@ -150,4 +150,3 @@ export const OTHER_CONNECTORS = [ // Re-export IndexingConfigState from schemas for backward compatibility export type { IndexingConfigState } from "./connector-popup.schemas"; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts index 89d8553b6..3fcdf352f 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-popup.schemas.ts @@ -80,7 +80,7 @@ export function parseConnectorPopupQueryParams( params: URLSearchParams | Record ): ConnectorPopupQueryParams { const obj: Record = {}; - + if (params instanceof URLSearchParams) { params.forEach((value, key) => { obj[key] = value || undefined; @@ -90,7 +90,7 @@ export function parseConnectorPopupQueryParams( obj[key] = value || undefined; }); } - + return connectorPopupQueryParamsSchema.parse(obj); } @@ -107,4 +107,3 @@ export function parseOAuthAuthResponse(data: unknown): OAuthAuthResponse { export function validateIndexingConfigState(data: unknown): IndexingConfigState { return indexingConfigStateSchema.parse(data); } - diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index a03551e31..fa35dda02 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -2,7 +2,12 @@ import { useAtomValue } from "jotai"; import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; -import { createConnectorMutationAtom, deleteConnectorMutationAtom, indexConnectorMutationAtom, updateConnectorMutationAtom } from "@/atoms/connectors/connector-mutation.atoms"; +import { + createConnectorMutationAtom, + deleteConnectorMutationAtom, + indexConnectorMutationAtom, + updateConnectorMutationAtom, +} from "@/atoms/connectors/connector-mutation.atoms"; import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms"; import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { authenticatedFetch } from "@/lib/auth-utils"; @@ -39,20 +44,23 @@ export const useConnectorDialog = () => { const [searchQuery, setSearchQuery] = useState(""); const [indexingConfig, setIndexingConfig] = useState(null); const [indexingConnector, setIndexingConnector] = useState(null); - const [indexingConnectorConfig, setIndexingConnectorConfig] = useState | null>(null); + const [indexingConnectorConfig, setIndexingConnectorConfig] = useState | null>(null); const [startDate, setStartDate] = useState(undefined); const [endDate, setEndDate] = useState(undefined); const [isStartingIndexing, setIsStartingIndexing] = useState(false); const [periodicEnabled, setPeriodicEnabled] = useState(false); const [frequencyMinutes, setFrequencyMinutes] = useState("1440"); - + // Edit mode state const [editingConnector, setEditingConnector] = useState(null); const [isSaving, setIsSaving] = useState(false); const [isDisconnecting, setIsDisconnecting] = useState(false); const [connectorConfig, setConnectorConfig] = useState | null>(null); const [connectorName, setConnectorName] = useState(null); - + // Connect mode state (for non-OAuth connectors) const [connectingConnectorType, setConnectingConnectorType] = useState(null); const [isCreatingConnector, setIsCreatingConnector] = useState(false); @@ -61,13 +69,20 @@ export const useConnectorDialog = () => { // Helper function to get frequency label const getFrequencyLabel = useCallback((minutes: string): string => { switch (minutes) { - case "15": return "15 minutes"; - case "60": return "hour"; - case "360": return "6 hours"; - case "720": return "12 hours"; - case "1440": return "day"; - case "10080": return "week"; - default: return `${minutes} minutes`; + case "15": + return "15 minutes"; + case "60": + return "hour"; + case "360": + return "6 hours"; + case "720": + return "12 hours"; + case "1440": + return "day"; + case "10080": + return "week"; + default: + return `${minutes} minutes`; } }, []); @@ -75,42 +90,42 @@ export const useConnectorDialog = () => { useEffect(() => { try { const params = parseConnectorPopupQueryParams(searchParams); - + if (params.modal === "connectors") { setIsOpen(true); - + if (params.tab === "active" || params.tab === "all") { setActiveTab(params.tab); } - + // Clear indexing config if view is not "configure" anymore if (params.view !== "configure" && indexingConfig) { setIndexingConfig(null); } - + // Clear editing connector if view is not "edit" anymore if (params.view !== "edit" && editingConnector) { setEditingConnector(null); setConnectorName(null); } - + // Clear connecting connector type if view is not "connect" anymore if (params.view !== "connect" && connectingConnectorType) { setConnectingConnectorType(null); } - + // Handle connect view if (params.view === "connect" && params.connectorType && !connectingConnectorType) { setConnectingConnectorType(params.connectorType); } - + // Handle YouTube view if (params.view === "youtube") { // YouTube view is active - no additional state needed } - + if (params.view === "configure" && params.connector && !indexingConfig) { - const oauthConnector = OAUTH_CONNECTORS.find(c => c.id === params.connector); + const oauthConnector = OAUTH_CONNECTORS.find((c) => c.id === params.connector); if (oauthConnector && allConnectors) { const existingConnector = allConnectors.find( (c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType @@ -131,7 +146,7 @@ export const useConnectorDialog = () => { } } } - + // Handle edit view if (params.view === "edit" && params.connectorId && allConnectors && !editingConnector) { const connectorId = parseInt(params.connectorId, 10); @@ -143,10 +158,12 @@ export const useConnectorDialog = () => { setConnectorConfig(connector.config); setConnectorName(connector.name); // Load existing periodic sync settings (disabled for Google Drive and non-indexable connectors) - setPeriodicEnabled(connector.connector_type === "GOOGLE_DRIVE_CONNECTOR" || !connector.is_indexable ? false : connector.periodic_indexing_enabled); - setFrequencyMinutes( - connector.indexing_frequency_minutes?.toString() || "1440" + setPeriodicEnabled( + connector.connector_type === "GOOGLE_DRIVE_CONNECTOR" || !connector.is_indexable + ? false + : connector.periodic_indexing_enabled ); + setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440"); // Reset dates - user can set new ones for re-indexing setStartDate(undefined); setEndDate(undefined); @@ -196,13 +213,18 @@ export const useConnectorDialog = () => { useEffect(() => { try { const params = parseConnectorPopupQueryParams(searchParams); - - if (params.success === "true" && params.connector && searchSpaceId && params.modal === "connectors") { - const oauthConnector = OAUTH_CONNECTORS.find(c => c.id === params.connector); + + if ( + params.success === "true" && + params.connector && + searchSpaceId && + params.modal === "connectors" + ) { + const oauthConnector = OAUTH_CONNECTORS.find((c) => c.id === params.connector); if (oauthConnector) { refetchAllConnectors().then((result) => { if (!result.data) return; - + const newConnector = result.data.find( (c: SearchSourceConnector) => c.connector_type === oauthConnector.connectorType ); @@ -256,10 +278,10 @@ export const useConnectorDialog = () => { } const data = await response.json(); - + // Validate OAuth response with Zod const validatedData = parseOAuthAuthResponse(data); - + // Don't clear connectingId here - let the redirect happen with button still disabled // The component will unmount on redirect anyway window.location.href = validatedData.auth_url; @@ -280,7 +302,7 @@ export const useConnectorDialog = () => { // Handle creating YouTube crawler (not a connector, shows view in popup) const handleCreateYouTubeCrawler = useCallback(() => { if (!searchSpaceId) return; - + // Update URL to show YouTube view const url = new URL(window.location.href); url.searchParams.set("modal", "connectors"); @@ -344,23 +366,26 @@ export const useConnectorDialog = () => { }, [searchSpaceId, createConnector, refetchAllConnectors]); // Handle connecting non-OAuth connectors (like Tavily API) - const handleConnectNonOAuth = useCallback((connectorType: string) => { - if (!searchSpaceId) return; - - // Set connecting state - setConnectingConnectorType(connectorType); - - // Update URL to show connect view - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("view", "connect"); - url.searchParams.set("connectorType", connectorType); - window.history.pushState({ modal: true }, "", url.toString()); - }, [searchSpaceId]); + const handleConnectNonOAuth = useCallback( + (connectorType: string) => { + if (!searchSpaceId) return; + + // Set connecting state + setConnectingConnectorType(connectorType); + + // Update URL to show connect view + const url = new URL(window.location.href); + url.searchParams.set("modal", "connectors"); + url.searchParams.set("view", "connect"); + url.searchParams.set("connectorType", connectorType); + window.history.pushState({ modal: true }, "", url.toString()); + }, + [searchSpaceId] + ); // Handle submitting connect form - const handleSubmitConnectForm = useCallback(async ( - formData: { + const handleSubmitConnectForm = useCallback( + async (formData: { name: string; connector_type: string; config: Record; @@ -373,179 +398,125 @@ export const useConnectorDialog = () => { endDate?: Date; periodicEnabled?: boolean; frequencyMinutes?: string; - } - ) => { - if (!searchSpaceId || !connectingConnectorType) return; - - // Prevent multiple submissions using ref for immediate check - if (isCreatingConnectorRef.current) return; - isCreatingConnectorRef.current = true; + }) => { + if (!searchSpaceId || !connectingConnectorType) return; - setIsCreatingConnector(true); - try { - // Extract UI-only fields before sending to backend - const { startDate, endDate, periodicEnabled, frequencyMinutes, ...connectorData } = formData; - - // Create connector - ensure types match the schema - const newConnector = await createConnector({ - data: { - ...connectorData, - connector_type: connectorData.connector_type as EnumConnectorName, - next_scheduled_at: connectorData.next_scheduled_at as string | null, - }, - queryParams: { - search_space_id: searchSpaceId, - }, - }); + // Prevent multiple submissions using ref for immediate check + if (isCreatingConnectorRef.current) return; + isCreatingConnectorRef.current = true; - // Refetch connectors to get the new one - const result = await refetchAllConnectors(); - if (result.data) { - const connector = result.data.find( - (c: SearchSourceConnector) => c.id === newConnector.id - ); - if (connector) { - // Validate connector data - const connectorValidation = searchSourceConnector.safeParse(connector); - if (connectorValidation.success) { - // Store connectingConnectorType before clearing it - const currentConnectorType = connectingConnectorType; - - // Find connector title from constants - const connectorInfo = OTHER_CONNECTORS.find( - c => c.connectorType === currentConnectorType - ); - const connectorTitle = connectorInfo?.title || connector.name; - - // Set up indexing config - const config = validateIndexingConfigState({ - connectorType: currentConnectorType as EnumConnectorName, - connectorId: connector.id, - connectorTitle, - }); - - // Clear connecting state to allow view transition - setConnectingConnectorType(null); - - // Set indexing config state - setIndexingConfig(config); - setIndexingConnector(connector); - setIndexingConnectorConfig(connector.config || {}); - - // Pre-populate indexing configuration with values from form if provided - if (formData.startDate !== undefined) { - setStartDate(formData.startDate); - } - if (formData.endDate !== undefined) { - setEndDate(formData.endDate); - } - if (formData.periodicEnabled !== undefined) { - setPeriodicEnabled(formData.periodicEnabled); - } - if (formData.frequencyMinutes !== undefined) { - setFrequencyMinutes(formData.frequencyMinutes); - } - - // Auto-start indexing for non-OAuth reindexable connectors - // This only applies to non-OAuth reindexable connectors (e.g., Elasticsearch, Linear) - // Non-reindexable connectors (e.g., Tavily) have is_indexable: false, so they won't trigger this - // Backend will use default date ranges (365 days ago to today) if dates are not provided - if (connector.is_indexable) { - // Get indexing configuration from form (or use defaults) - const startDateForIndexing = formData.startDate; - const endDateForIndexing = formData.endDate; - const periodicEnabledForIndexing = formData.periodicEnabled || false; - const frequencyMinutesForIndexing = formData.frequencyMinutes || "1440"; - - // Update connector with periodic sync settings if enabled - if (periodicEnabledForIndexing) { - const frequency = parseInt(frequencyMinutesForIndexing, 10); - await updateConnector({ - id: connector.id, - data: { - periodic_indexing_enabled: true, - indexing_frequency_minutes: frequency, + setIsCreatingConnector(true); + try { + // Extract UI-only fields before sending to backend + const { startDate, endDate, periodicEnabled, frequencyMinutes, ...connectorData } = + formData; + + // Create connector - ensure types match the schema + const newConnector = await createConnector({ + data: { + ...connectorData, + connector_type: connectorData.connector_type as EnumConnectorName, + next_scheduled_at: connectorData.next_scheduled_at as string | null, + }, + queryParams: { + search_space_id: searchSpaceId, + }, + }); + + // Refetch connectors to get the new one + const result = await refetchAllConnectors(); + if (result.data) { + const connector = result.data.find( + (c: SearchSourceConnector) => c.id === newConnector.id + ); + if (connector) { + // Validate connector data + const connectorValidation = searchSourceConnector.safeParse(connector); + if (connectorValidation.success) { + // Store connectingConnectorType before clearing it + const currentConnectorType = connectingConnectorType; + + // Find connector title from constants + const connectorInfo = OTHER_CONNECTORS.find( + (c) => c.connectorType === currentConnectorType + ); + const connectorTitle = connectorInfo?.title || connector.name; + + // Set up indexing config + const config = validateIndexingConfigState({ + connectorType: currentConnectorType as EnumConnectorName, + connectorId: connector.id, + connectorTitle, + }); + + // Clear connecting state to allow view transition + setConnectingConnectorType(null); + + // Set indexing config state + setIndexingConfig(config); + setIndexingConnector(connector); + setIndexingConnectorConfig(connector.config || {}); + + // Pre-populate indexing configuration with values from form if provided + if (formData.startDate !== undefined) { + setStartDate(formData.startDate); + } + if (formData.endDate !== undefined) { + setEndDate(formData.endDate); + } + if (formData.periodicEnabled !== undefined) { + setPeriodicEnabled(formData.periodicEnabled); + } + if (formData.frequencyMinutes !== undefined) { + setFrequencyMinutes(formData.frequencyMinutes); + } + + // Auto-start indexing for non-OAuth reindexable connectors + // This only applies to non-OAuth reindexable connectors (e.g., Elasticsearch, Linear) + // Non-reindexable connectors (e.g., Tavily) have is_indexable: false, so they won't trigger this + // Backend will use default date ranges (365 days ago to today) if dates are not provided + if (connector.is_indexable) { + // Get indexing configuration from form (or use defaults) + const startDateForIndexing = formData.startDate; + const endDateForIndexing = formData.endDate; + const periodicEnabledForIndexing = formData.periodicEnabled || false; + const frequencyMinutesForIndexing = formData.frequencyMinutes || "1440"; + + // Update connector with periodic sync settings if enabled + if (periodicEnabledForIndexing) { + const frequency = parseInt(frequencyMinutesForIndexing, 10); + await updateConnector({ + id: connector.id, + data: { + periodic_indexing_enabled: true, + indexing_frequency_minutes: frequency, + }, + }); + } + + // Start indexing (backend will use defaults if dates are undefined) + const startDateStr = startDateForIndexing + ? format(startDateForIndexing, "yyyy-MM-dd") + : undefined; + const endDateStr = endDateForIndexing + ? format(endDateForIndexing, "yyyy-MM-dd") + : undefined; + + await indexConnector({ + connector_id: connector.id, + queryParams: { + search_space_id: searchSpaceId, + start_date: startDateStr, + end_date: endDateStr, }, }); - } - - // Start indexing (backend will use defaults if dates are undefined) - const startDateStr = startDateForIndexing ? format(startDateForIndexing, "yyyy-MM-dd") : undefined; - const endDateStr = endDateForIndexing ? format(endDateForIndexing, "yyyy-MM-dd") : undefined; - - await indexConnector({ - connector_id: connector.id, - queryParams: { - search_space_id: searchSpaceId, - start_date: startDateStr, - end_date: endDateStr, - }, - }); - - toast.success(`${connectorTitle} connected and indexing started!`, { - description: periodicEnabledForIndexing - ? `Periodic sync enabled every ${getFrequencyLabel(frequencyMinutesForIndexing)}.` - : "You can continue working while we sync your data.", - }); - - // Close modal and return to main view - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("view"); - url.searchParams.delete("connectorType"); - router.replace(url.pathname + url.search, { scroll: false }); - - // Clear indexing config state since we're not showing the view - setIndexingConfig(null); - setIndexingConnector(null); - setIndexingConnectorConfig(null); - - // Invalidate queries to refresh data - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); - - // Refresh connectors list - await refetchAllConnectors(); - } else { - // Non-indexable connector - // For Circleback, transition to edit view to show webhook URL - // For other non-indexable connectors, just close the modal - if (currentConnectorType === "CIRCLEBACK_CONNECTOR") { - // Clear connecting state and indexing config state - setConnectingConnectorType(null); - setIndexingConfig(null); - setIndexingConnector(null); - setIndexingConnectorConfig(null); - - // Set up edit view state - setEditingConnector(connector); - setConnectorName(connector.name); - setConnectorConfig(connector.config || {}); - setPeriodicEnabled(false); - setFrequencyMinutes("1440"); - setStartDate(undefined); - setEndDate(undefined); - - toast.success(`${connectorTitle} connected successfully!`, { - description: "Configure the webhook URL in your Circleback settings.", + + toast.success(`${connectorTitle} connected and indexing started!`, { + description: periodicEnabledForIndexing + ? `Periodic sync enabled every ${getFrequencyLabel(frequencyMinutesForIndexing)}.` + : "You can continue working while we sync your data.", }); - - // Transition to edit view - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("view", "edit"); - url.searchParams.set("connectorId", connector.id.toString()); - url.searchParams.delete("connectorType"); - router.replace(url.pathname + url.search, { scroll: false }); - - // Refresh connectors list - await refetchAllConnectors(); - } else { - // Other non-indexable connectors - just show success message and close - toast.success(`${connectorTitle} connected successfully!`); - + // Close modal and return to main view const url = new URL(window.location.href); url.searchParams.delete("modal"); @@ -553,25 +524,94 @@ export const useConnectorDialog = () => { url.searchParams.delete("view"); url.searchParams.delete("connectorType"); router.replace(url.pathname + url.search, { scroll: false }); - - // Clear indexing config state + + // Clear indexing config state since we're not showing the view setIndexingConfig(null); setIndexingConnector(null); setIndexingConnectorConfig(null); + + // Invalidate queries to refresh data + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), + }); + + // Refresh connectors list + await refetchAllConnectors(); + } else { + // Non-indexable connector + // For Circleback, transition to edit view to show webhook URL + // For other non-indexable connectors, just close the modal + if (currentConnectorType === "CIRCLEBACK_CONNECTOR") { + // Clear connecting state and indexing config state + setConnectingConnectorType(null); + setIndexingConfig(null); + setIndexingConnector(null); + setIndexingConnectorConfig(null); + + // Set up edit view state + setEditingConnector(connector); + setConnectorName(connector.name); + setConnectorConfig(connector.config || {}); + setPeriodicEnabled(false); + setFrequencyMinutes("1440"); + setStartDate(undefined); + setEndDate(undefined); + + toast.success(`${connectorTitle} connected successfully!`, { + description: "Configure the webhook URL in your Circleback settings.", + }); + + // Transition to edit view + const url = new URL(window.location.href); + url.searchParams.set("modal", "connectors"); + url.searchParams.set("view", "edit"); + url.searchParams.set("connectorId", connector.id.toString()); + url.searchParams.delete("connectorType"); + router.replace(url.pathname + url.search, { scroll: false }); + + // Refresh connectors list + await refetchAllConnectors(); + } else { + // Other non-indexable connectors - just show success message and close + toast.success(`${connectorTitle} connected successfully!`); + + // Close modal and return to main view + const url = new URL(window.location.href); + url.searchParams.delete("modal"); + url.searchParams.delete("tab"); + url.searchParams.delete("view"); + url.searchParams.delete("connectorType"); + router.replace(url.pathname + url.search, { scroll: false }); + + // Clear indexing config state + setIndexingConfig(null); + setIndexingConnector(null); + setIndexingConnectorConfig(null); + } } } } } + } catch (error) { + console.error("Error creating connector:", error); + toast.error(error instanceof Error ? error.message : "Failed to create connector"); + } finally { + isCreatingConnectorRef.current = false; + setIsCreatingConnector(false); + // Don't clear connectingConnectorType here - it's cleared above when transitioning to config view } - } catch (error) { - console.error("Error creating connector:", error); - toast.error(error instanceof Error ? error.message : "Failed to create connector"); - } finally { - isCreatingConnectorRef.current = false; - setIsCreatingConnector(false); - // Don't clear connectingConnectorType here - it's cleared above when transitioning to config view - } - }, [connectingConnectorType, searchSpaceId, createConnector, refetchAllConnectors, updateConnector, indexConnector, router, getFrequencyLabel]); + }, + [ + connectingConnectorType, + searchSpaceId, + createConnector, + refetchAllConnectors, + updateConnector, + indexConnector, + router, + getFrequencyLabel, + ] + ); // Handle going back from connect view const handleBackFromConnect = useCallback(() => { @@ -593,125 +633,151 @@ export const useConnectorDialog = () => { }, [router]); // Handle starting indexing - const handleStartIndexing = useCallback(async (refreshConnectors: () => void) => { - if (!indexingConfig || !searchSpaceId) return; + const handleStartIndexing = useCallback( + async (refreshConnectors: () => void) => { + if (!indexingConfig || !searchSpaceId) return; - // Validate date range (skip for Google Drive and Webcrawler) - if (indexingConfig.connectorType !== "GOOGLE_DRIVE_CONNECTOR" && indexingConfig.connectorType !== "WEBCRAWLER_CONNECTOR") { - const dateRangeValidation = dateRangeSchema.safeParse({ startDate, endDate }); - if (!dateRangeValidation.success) { - const firstIssueMsg = - dateRangeValidation.error.issues && dateRangeValidation.error.issues.length > 0 - ? dateRangeValidation.error.issues[0].message - : "Invalid date range"; - toast.error(firstIssueMsg); - return; - } - } - - // Validate frequency minutes if periodic is enabled - if (periodicEnabled) { - const frequencyValidation = frequencyMinutesSchema.safeParse(frequencyMinutes); - if (!frequencyValidation.success) { - toast.error("Invalid frequency value"); - return; - } - } - - setIsStartingIndexing(true); - try { - const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined; - const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined; - - // Update connector with periodic sync settings and config changes - // Note: Periodic sync is disabled for Google Drive connectors - if (periodicEnabled || indexingConnectorConfig) { - const frequency = periodicEnabled ? parseInt(frequencyMinutes, 10) : undefined; - await updateConnector({ - id: indexingConfig.connectorId, - data: { - ...(periodicEnabled && indexingConfig.connectorType !== "GOOGLE_DRIVE_CONNECTOR" && { - periodic_indexing_enabled: true, - indexing_frequency_minutes: frequency, - }), - ...(indexingConfig.connectorType === "GOOGLE_DRIVE_CONNECTOR" && { - periodic_indexing_enabled: false, - indexing_frequency_minutes: null, - }), - ...(indexingConnectorConfig && { - config: indexingConnectorConfig, - }), - }, - }); + // Validate date range (skip for Google Drive and Webcrawler) + if ( + indexingConfig.connectorType !== "GOOGLE_DRIVE_CONNECTOR" && + indexingConfig.connectorType !== "WEBCRAWLER_CONNECTOR" + ) { + const dateRangeValidation = dateRangeSchema.safeParse({ startDate, endDate }); + if (!dateRangeValidation.success) { + const firstIssueMsg = + dateRangeValidation.error.issues && dateRangeValidation.error.issues.length > 0 + ? dateRangeValidation.error.issues[0].message + : "Invalid date range"; + toast.error(firstIssueMsg); + return; + } } - // Handle Google Drive folder selection - if (indexingConfig.connectorType === "GOOGLE_DRIVE_CONNECTOR" && indexingConnectorConfig) { - const selectedFolders = indexingConnectorConfig.selected_folders as Array<{ id: string; name: string }> | undefined; - const selectedFiles = indexingConnectorConfig.selected_files as Array<{ id: string; name: string }> | undefined; - if ((selectedFolders && selectedFolders.length > 0) || (selectedFiles && selectedFiles.length > 0)) { - // Index with folder/file selection + // Validate frequency minutes if periodic is enabled + if (periodicEnabled) { + const frequencyValidation = frequencyMinutesSchema.safeParse(frequencyMinutes); + if (!frequencyValidation.success) { + toast.error("Invalid frequency value"); + return; + } + } + + setIsStartingIndexing(true); + try { + const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined; + const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined; + + // Update connector with periodic sync settings and config changes + // Note: Periodic sync is disabled for Google Drive connectors + if (periodicEnabled || indexingConnectorConfig) { + const frequency = periodicEnabled ? parseInt(frequencyMinutes, 10) : undefined; + await updateConnector({ + id: indexingConfig.connectorId, + data: { + ...(periodicEnabled && + indexingConfig.connectorType !== "GOOGLE_DRIVE_CONNECTOR" && { + periodic_indexing_enabled: true, + indexing_frequency_minutes: frequency, + }), + ...(indexingConfig.connectorType === "GOOGLE_DRIVE_CONNECTOR" && { + periodic_indexing_enabled: false, + indexing_frequency_minutes: null, + }), + ...(indexingConnectorConfig && { + config: indexingConnectorConfig, + }), + }, + }); + } + + // Handle Google Drive folder selection + if (indexingConfig.connectorType === "GOOGLE_DRIVE_CONNECTOR" && indexingConnectorConfig) { + const selectedFolders = indexingConnectorConfig.selected_folders as + | Array<{ id: string; name: string }> + | undefined; + const selectedFiles = indexingConnectorConfig.selected_files as + | Array<{ id: string; name: string }> + | undefined; + if ( + (selectedFolders && selectedFolders.length > 0) || + (selectedFiles && selectedFiles.length > 0) + ) { + // Index with folder/file selection + await indexConnector({ + connector_id: indexingConfig.connectorId, + queryParams: { + search_space_id: searchSpaceId, + }, + body: { + folders: selectedFolders || [], + files: selectedFiles || [], + }, + }); + } else { + // Google Drive requires folder selection - show error if none selected + toast.error("Please select at least one folder to index"); + setIsStartingIndexing(false); + return; + } + } else if (indexingConfig.connectorType === "WEBCRAWLER_CONNECTOR") { + // Webcrawler doesn't use date ranges, just uses config (API key and URLs) await indexConnector({ connector_id: indexingConfig.connectorId, queryParams: { search_space_id: searchSpaceId, }, - body: { - folders: selectedFolders || [], - files: selectedFiles || [], - }, }); } else { - // Google Drive requires folder selection - show error if none selected - toast.error("Please select at least one folder to index"); - setIsStartingIndexing(false); - return; + await indexConnector({ + connector_id: indexingConfig.connectorId, + queryParams: { + search_space_id: searchSpaceId, + start_date: startDateStr, + end_date: endDateStr, + }, + }); } - } else if (indexingConfig.connectorType === "WEBCRAWLER_CONNECTOR") { - // Webcrawler doesn't use date ranges, just uses config (API key and URLs) - await indexConnector({ - connector_id: indexingConfig.connectorId, - queryParams: { - search_space_id: searchSpaceId, - }, + + toast.success(`${indexingConfig.connectorTitle} indexing started`, { + description: periodicEnabled + ? `Periodic sync enabled every ${getFrequencyLabel(frequencyMinutes)}.` + : "You can continue working while we sync your data.", }); - } else { - await indexConnector({ - connector_id: indexingConfig.connectorId, - queryParams: { - search_space_id: searchSpaceId, - start_date: startDateStr, - end_date: endDateStr, - }, + + // Update URL - the effect will handle closing the modal and clearing state + const url = new URL(window.location.href); + url.searchParams.delete("modal"); + url.searchParams.delete("tab"); + url.searchParams.delete("success"); + url.searchParams.delete("connector"); + url.searchParams.delete("view"); + router.replace(url.pathname + url.search, { scroll: false }); + + refreshConnectors(); + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), }); + } catch (error) { + console.error("Error starting indexing:", error); + toast.error("Failed to start indexing"); + } finally { + setIsStartingIndexing(false); } - - toast.success(`${indexingConfig.connectorTitle} indexing started`, { - description: periodicEnabled - ? `Periodic sync enabled every ${getFrequencyLabel(frequencyMinutes)}.` - : "You can continue working while we sync your data.", - }); - - // Update URL - the effect will handle closing the modal and clearing state - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("success"); - url.searchParams.delete("connector"); - url.searchParams.delete("view"); - router.replace(url.pathname + url.search, { scroll: false }); - - refreshConnectors(); - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); - } catch (error) { - console.error("Error starting indexing:", error); - toast.error("Failed to start indexing"); - } finally { - setIsStartingIndexing(false); - } - }, [indexingConfig, searchSpaceId, startDate, endDate, indexConnector, updateConnector, periodicEnabled, frequencyMinutes, getFrequencyLabel, router, indexingConnectorConfig]); + }, + [ + indexingConfig, + searchSpaceId, + startDate, + endDate, + indexConnector, + updateConnector, + periodicEnabled, + frequencyMinutes, + getFrequencyLabel, + router, + indexingConnectorConfig, + ] + ); // Handle skipping indexing const handleSkipIndexing = useCallback(() => { @@ -726,207 +792,256 @@ export const useConnectorDialog = () => { }, [router]); // Handle starting edit mode - const handleStartEdit = useCallback((connector: SearchSourceConnector) => { - if (!searchSpaceId) return; - - // All connector types should be handled in the popup edit view - // Validate connector data - const connectorValidation = searchSourceConnector.safeParse(connector); - if (!connectorValidation.success) { - toast.error("Invalid connector data"); - return; - } - - setEditingConnector(connector); - setConnectorName(connector.name); - // Load existing periodic sync settings (disabled for Google Drive and non-indexable connectors) - setPeriodicEnabled(connector.connector_type === "GOOGLE_DRIVE_CONNECTOR" || !connector.is_indexable ? false : connector.periodic_indexing_enabled); - setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440"); - // Reset dates - user can set new ones for re-indexing - setStartDate(undefined); - setEndDate(undefined); - - // Update URL - const url = new URL(window.location.href); - url.searchParams.set("modal", "connectors"); - url.searchParams.set("view", "edit"); - url.searchParams.set("connectorId", connector.id.toString()); - window.history.pushState({ modal: true }, "", url.toString()); - }, [searchSpaceId]); + const handleStartEdit = useCallback( + (connector: SearchSourceConnector) => { + if (!searchSpaceId) return; + + // All connector types should be handled in the popup edit view + // Validate connector data + const connectorValidation = searchSourceConnector.safeParse(connector); + if (!connectorValidation.success) { + toast.error("Invalid connector data"); + return; + } + + setEditingConnector(connector); + setConnectorName(connector.name); + // Load existing periodic sync settings (disabled for Google Drive and non-indexable connectors) + setPeriodicEnabled( + connector.connector_type === "GOOGLE_DRIVE_CONNECTOR" || !connector.is_indexable + ? false + : connector.periodic_indexing_enabled + ); + setFrequencyMinutes(connector.indexing_frequency_minutes?.toString() || "1440"); + // Reset dates - user can set new ones for re-indexing + setStartDate(undefined); + setEndDate(undefined); + + // Update URL + const url = new URL(window.location.href); + url.searchParams.set("modal", "connectors"); + url.searchParams.set("view", "edit"); + url.searchParams.set("connectorId", connector.id.toString()); + window.history.pushState({ modal: true }, "", url.toString()); + }, + [searchSpaceId] + ); // Handle saving connector changes - const handleSaveConnector = useCallback(async (refreshConnectors: () => void) => { - if (!editingConnector || !searchSpaceId) return; + const handleSaveConnector = useCallback( + async (refreshConnectors: () => void) => { + if (!editingConnector || !searchSpaceId) return; - // Validate date range (skip for Google Drive which uses folder selection, Webcrawler which uses config, and non-indexable connectors) - if (editingConnector.is_indexable && editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && editingConnector.connector_type !== "WEBCRAWLER_CONNECTOR") { - const dateRangeValidation = dateRangeSchema.safeParse({ startDate, endDate }); - if (!dateRangeValidation.success) { - toast.error(dateRangeValidation.error.issues[0]?.message || "Invalid date range"); + // Validate date range (skip for Google Drive which uses folder selection, Webcrawler which uses config, and non-indexable connectors) + if ( + editingConnector.is_indexable && + editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && + editingConnector.connector_type !== "WEBCRAWLER_CONNECTOR" + ) { + const dateRangeValidation = dateRangeSchema.safeParse({ startDate, endDate }); + if (!dateRangeValidation.success) { + toast.error(dateRangeValidation.error.issues[0]?.message || "Invalid date range"); + return; + } + } + + // Prevent periodic indexing for non-indexable connectors + if (periodicEnabled && !editingConnector.is_indexable) { + toast.error("Periodic indexing is not available for this connector type"); return; } - } - // Prevent periodic indexing for non-indexable connectors - if (periodicEnabled && !editingConnector.is_indexable) { - toast.error("Periodic indexing is not available for this connector type"); - return; - } - - // Validate frequency minutes if periodic is enabled (only for indexable connectors) - if (periodicEnabled && editingConnector.is_indexable) { - const frequencyValidation = frequencyMinutesSchema.safeParse(frequencyMinutes); - if (!frequencyValidation.success) { - toast.error("Invalid frequency value"); - return; + // Validate frequency minutes if periodic is enabled (only for indexable connectors) + if (periodicEnabled && editingConnector.is_indexable) { + const frequencyValidation = frequencyMinutesSchema.safeParse(frequencyMinutes); + if (!frequencyValidation.success) { + toast.error("Invalid frequency value"); + return; + } } - } - setIsSaving(true); - try { - const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined; - const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined; + setIsSaving(true); + try { + const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined; + const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined; - // Update connector with periodic sync settings, config changes, and name - // Note: Periodic sync is disabled for Google Drive connectors and non-indexable connectors - const frequency = periodicEnabled && editingConnector.is_indexable ? parseInt(frequencyMinutes, 10) : null; - await updateConnector({ - id: editingConnector.id, - data: { - name: connectorName || editingConnector.name, - periodic_indexing_enabled: editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" || !editingConnector.is_indexable ? false : periodicEnabled, - indexing_frequency_minutes: editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" || !editingConnector.is_indexable ? null : frequency, - config: connectorConfig || editingConnector.config, - }, - }); + // Update connector with periodic sync settings, config changes, and name + // Note: Periodic sync is disabled for Google Drive connectors and non-indexable connectors + const frequency = + periodicEnabled && editingConnector.is_indexable ? parseInt(frequencyMinutes, 10) : null; + await updateConnector({ + id: editingConnector.id, + data: { + name: connectorName || editingConnector.name, + periodic_indexing_enabled: + editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" || + !editingConnector.is_indexable + ? false + : periodicEnabled, + indexing_frequency_minutes: + editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR" || + !editingConnector.is_indexable + ? null + : frequency, + config: connectorConfig || editingConnector.config, + }, + }); - // Re-index based on connector type (only for indexable connectors) - let indexingDescription = "Settings saved."; - if (!editingConnector.is_indexable) { - // Non-indexable connectors (like Tavily API) don't need re-indexing - indexingDescription = "Settings saved."; - } else if (editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR") { - // Google Drive uses folder selection from config, not date ranges - const selectedFolders = (connectorConfig || editingConnector.config)?.selected_folders as Array<{ id: string; name: string }> | undefined; - const selectedFiles = (connectorConfig || editingConnector.config)?.selected_files as Array<{ id: string; name: string }> | undefined; - if ((selectedFolders && selectedFolders.length > 0) || (selectedFiles && selectedFiles.length > 0)) { + // Re-index based on connector type (only for indexable connectors) + let indexingDescription = "Settings saved."; + if (!editingConnector.is_indexable) { + // Non-indexable connectors (like Tavily API) don't need re-indexing + indexingDescription = "Settings saved."; + } else if (editingConnector.connector_type === "GOOGLE_DRIVE_CONNECTOR") { + // Google Drive uses folder selection from config, not date ranges + const selectedFolders = (connectorConfig || editingConnector.config)?.selected_folders as + | Array<{ id: string; name: string }> + | undefined; + const selectedFiles = (connectorConfig || editingConnector.config)?.selected_files as + | Array<{ id: string; name: string }> + | undefined; + if ( + (selectedFolders && selectedFolders.length > 0) || + (selectedFiles && selectedFiles.length > 0) + ) { + await indexConnector({ + connector_id: editingConnector.id, + queryParams: { + search_space_id: searchSpaceId, + }, + body: { + folders: selectedFolders || [], + files: selectedFiles || [], + }, + }); + const totalItems = (selectedFolders?.length || 0) + (selectedFiles?.length || 0); + indexingDescription = `Re-indexing started for ${totalItems} item(s).`; + } + } else if (editingConnector.connector_type === "WEBCRAWLER_CONNECTOR") { + // Webcrawler uses config (API key and URLs), not date ranges await indexConnector({ connector_id: editingConnector.id, queryParams: { search_space_id: searchSpaceId, }, - body: { - folders: selectedFolders || [], - files: selectedFiles || [], + }); + indexingDescription = "Re-indexing started with updated configuration."; + } else if (startDateStr || endDateStr) { + // Other connectors use date ranges + await indexConnector({ + connector_id: editingConnector.id, + queryParams: { + search_space_id: searchSpaceId, + start_date: startDateStr, + end_date: endDateStr, }, }); - const totalItems = (selectedFolders?.length || 0) + (selectedFiles?.length || 0); - indexingDescription = `Re-indexing started for ${totalItems} item(s).`; + indexingDescription = "Re-indexing started with new date range."; } - } else if (editingConnector.connector_type === "WEBCRAWLER_CONNECTOR") { - // Webcrawler uses config (API key and URLs), not date ranges - await indexConnector({ - connector_id: editingConnector.id, - queryParams: { - search_space_id: searchSpaceId, - }, + + toast.success(`${editingConnector.name} updated successfully`, { + description: periodicEnabled + ? `Periodic sync ${frequency ? `enabled every ${getFrequencyLabel(frequencyMinutes)}` : "enabled"}. ${indexingDescription}` + : indexingDescription, }); - indexingDescription = "Re-indexing started with updated configuration."; - } else if (startDateStr || endDateStr) { - // Other connectors use date ranges - await indexConnector({ - connector_id: editingConnector.id, - queryParams: { - search_space_id: searchSpaceId, - start_date: startDateStr, - end_date: endDateStr, - }, + + // Update URL - the effect will handle closing the modal and clearing state + const url = new URL(window.location.href); + url.searchParams.delete("modal"); + url.searchParams.delete("tab"); + url.searchParams.delete("view"); + url.searchParams.delete("connectorId"); + router.replace(url.pathname + url.search, { scroll: false }); + + refreshConnectors(); + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), }); - indexingDescription = "Re-indexing started with new date range."; + } catch (error) { + console.error("Error saving connector:", error); + toast.error("Failed to save connector changes"); + } finally { + setIsSaving(false); } - - toast.success(`${editingConnector.name} updated successfully`, { - description: periodicEnabled - ? `Periodic sync ${frequency ? `enabled every ${getFrequencyLabel(frequencyMinutes)}` : "enabled"}. ${indexingDescription}` - : indexingDescription, - }); - - // Update URL - the effect will handle closing the modal and clearing state - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("view"); - url.searchParams.delete("connectorId"); - router.replace(url.pathname + url.search, { scroll: false }); - - refreshConnectors(); - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); - } catch (error) { - console.error("Error saving connector:", error); - toast.error("Failed to save connector changes"); - } finally { - setIsSaving(false); - } - }, [editingConnector, searchSpaceId, startDate, endDate, indexConnector, updateConnector, periodicEnabled, frequencyMinutes, getFrequencyLabel, router, connectorConfig, connectorName]); + }, + [ + editingConnector, + searchSpaceId, + startDate, + endDate, + indexConnector, + updateConnector, + periodicEnabled, + frequencyMinutes, + getFrequencyLabel, + router, + connectorConfig, + connectorName, + ] + ); // Handle disconnecting connector - const handleDisconnectConnector = useCallback(async (refreshConnectors: () => void) => { - if (!editingConnector || !searchSpaceId) return; + const handleDisconnectConnector = useCallback( + async (refreshConnectors: () => void) => { + if (!editingConnector || !searchSpaceId) return; - setIsDisconnecting(true); - try { - await deleteConnector({ - id: editingConnector.id, - }); + setIsDisconnecting(true); + try { + await deleteConnector({ + id: editingConnector.id, + }); - toast.success(`${editingConnector.name} disconnected successfully`); + toast.success(`${editingConnector.name} disconnected successfully`); - // Update URL - the effect will handle closing the modal and clearing state - const url = new URL(window.location.href); - url.searchParams.delete("modal"); - url.searchParams.delete("tab"); - url.searchParams.delete("view"); - url.searchParams.delete("connectorId"); - router.replace(url.pathname + url.search, { scroll: false }); - - refreshConnectors(); - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); - } catch (error) { - console.error("Error disconnecting connector:", error); - toast.error("Failed to disconnect connector"); - } finally { - setIsDisconnecting(false); - } - }, [editingConnector, searchSpaceId, deleteConnector, router]); + // Update URL - the effect will handle closing the modal and clearing state + const url = new URL(window.location.href); + url.searchParams.delete("modal"); + url.searchParams.delete("tab"); + url.searchParams.delete("view"); + url.searchParams.delete("connectorId"); + router.replace(url.pathname + url.search, { scroll: false }); + + refreshConnectors(); + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), + }); + } catch (error) { + console.error("Error disconnecting connector:", error); + toast.error("Failed to disconnect connector"); + } finally { + setIsDisconnecting(false); + } + }, + [editingConnector, searchSpaceId, deleteConnector, router] + ); // Handle quick index (index without date picker, uses backend defaults) - const handleQuickIndexConnector = useCallback(async (connectorId: number) => { - if (!searchSpaceId) return; - - try { - await indexConnector({ - connector_id: connectorId, - queryParams: { - search_space_id: searchSpaceId, - }, - }); - toast.success("Indexing started", { - description: "You can continue working while we sync your data.", - }); - - // Invalidate queries to refresh data - queryClient.invalidateQueries({ - queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), - }); - } catch (error) { - console.error("Error indexing connector content:", error); - toast.error(error instanceof Error ? error.message : "Failed to start indexing"); - } - }, [searchSpaceId, indexConnector]); + const handleQuickIndexConnector = useCallback( + async (connectorId: number) => { + if (!searchSpaceId) return; + + try { + await indexConnector({ + connector_id: connectorId, + queryParams: { + search_space_id: searchSpaceId, + }, + }); + toast.success("Indexing started", { + description: "You can continue working while we sync your data.", + }); + + // Invalidate queries to refresh data + queryClient.invalidateQueries({ + queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), + }); + } catch (error) { + console.error("Error indexing connector content:", error); + toast.error(error instanceof Error ? error.message : "Failed to start indexing"); + } + }, + [searchSpaceId, indexConnector] + ); // Handle going back from edit view const handleBackFromEdit = useCallback(() => { @@ -977,22 +1092,19 @@ export const useConnectorDialog = () => { ); // Handle tab change - const handleTabChange = useCallback( - (value: string) => { - setActiveTab(value); - const url = new URL(window.location.href); - url.searchParams.set("tab", value); - window.history.replaceState({ modal: true }, "", url.toString()); - }, - [] - ); + const handleTabChange = useCallback((value: string) => { + setActiveTab(value); + const url = new URL(window.location.href); + url.searchParams.set("tab", value); + window.history.replaceState({ modal: true }, "", url.toString()); + }, []); // Handle scroll const handleScroll = useCallback((e: React.UIEvent) => { setIsScrolled(e.currentTarget.scrollTop > 0); }, []); - return { + return { // State isOpen, activeTab, @@ -1014,7 +1126,7 @@ export const useConnectorDialog = () => { frequencyMinutes, searchSpaceId, allConnectors, - + // Setters setSearchQuery, setStartDate, @@ -1022,7 +1134,7 @@ export const useConnectorDialog = () => { setPeriodicEnabled, setFrequencyMinutes, setConnectorName, - + // Handlers handleOpenChange, handleTabChange, @@ -1046,4 +1158,3 @@ export const useConnectorDialog = () => { setIndexingConnectorConfig, }; }; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/index.ts b/surfsense_web/components/assistant-ui/connector-popup/index.ts index 7d7b737fd..e2e2d8b30 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/index.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/index.ts @@ -35,4 +35,3 @@ export type { // Hooks export { useConnectorDialog } from "./hooks/use-connector-dialog"; - diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx index 95a90c481..c17afc84f 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx @@ -11,9 +11,7 @@ import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { LogSummary, LogActiveTask } from "@/contracts/types/log.types"; import { cn } from "@/lib/utils"; import { getDocumentCountForConnector } from "../utils/connector-document-mapping"; -import { - TabsContent, -} from "@/components/ui/tabs"; +import { TabsContent } from "@/components/ui/tabs"; interface ActiveConnectorsTabProps { hasSources: boolean; @@ -68,12 +66,10 @@ export const ActiveConnectorsTab: FC = ({ // These are: EXTENSION (browser extension), FILE (uploaded files), NOTE (editor notes), // YOUTUBE_VIDEO (YouTube videos), and CRAWLED_URL (web pages - shown separately even though it can come from WEBCRAWLER_CONNECTOR) const standaloneDocumentTypes = ["EXTENSION", "FILE", "NOTE", "YOUTUBE_VIDEO", "CRAWLED_URL"]; - + // Filter to only show standalone document types that have documents (count > 0) const standaloneDocuments = activeDocumentTypes - .filter(([docType, count]) => - standaloneDocumentTypes.includes(docType) && count > 0 - ) + .filter(([docType, count]) => standaloneDocumentTypes.includes(docType) && count > 0) .map(([docType, count]) => ({ type: docType, count, @@ -88,78 +84,76 @@ export const ActiveConnectorsTab: FC = ({ {connectors.length > 0 && (
-

- Active Connectors -

+

Active Connectors

{connectors.map((connector) => { - const isIndexing = indexingConnectorIds.has(connector.id); - const activeTask = logsSummary?.active_tasks?.find( - (task: LogActiveTask) => task.connector_id === connector.id - ); - const documentCount = getDocumentCountForConnector( - connector.connector_type, - documentTypeCounts - ); + const isIndexing = indexingConnectorIds.has(connector.id); + const activeTask = logsSummary?.active_tasks?.find( + (task: LogActiveTask) => task.connector_id === connector.id + ); + const documentCount = getDocumentCountForConnector( + connector.connector_type, + documentTypeCounts + ); - return ( -
-
- {getConnectorIcon(connector.connector_type, "size-6")} -
-
-

- {connector.name} -

- {isIndexing ? ( -

- - Indexing... - {activeTask?.message && ( - - • {activeTask.message} - + return ( +

+
- ) : ( -

- {connector.last_indexed_at - ? `Last indexed: ${format(new Date(connector.last_indexed_at), "MMM d, yyyy")}` - : "Never indexed"} -

- )} -

- {formatDocumentCount(documentCount)} -

-
- -
- ); - })} + > + {getConnectorIcon(connector.connector_type, "size-6")} +
+
+

+ {connector.name} +

+ {isIndexing ? ( +

+ + Indexing... + {activeTask?.message && ( + + • {activeTask.message} + + )} +

+ ) : ( +

+ {connector.last_indexed_at + ? `Last indexed: ${format(new Date(connector.last_indexed_at), "MMM d, yyyy")}` + : "Never indexed"} +

+ )} +

+ {formatDocumentCount(documentCount)} +

+
+ +
+ ); + })}
)} @@ -168,9 +162,7 @@ export const ActiveConnectorsTab: FC = ({ {standaloneDocuments.length > 0 && (
-

- Documents -

+

Documents

-

- {t("title")} -

-

- {t("subtitle")} -

+

{t("title")}

+

{t("subtitle")}

@@ -159,7 +152,8 @@ export const YouTubeCrawlerView: FC = ({ styleClasses={{ inlineTagsContainer: "border border-slate-400/20 rounded-lg bg-muted/50 shadow-sm shadow-black/5 transition-shadow focus-within:border-slate-400/40 focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 p-1 gap-1", - input: "w-full min-w-[80px] focus-visible:outline-none shadow-none px-2 h-7 text-foreground/90 placeholder:text-muted-foreground bg-transparent", + input: + "w-full min-w-[80px] focus-visible:outline-none shadow-none px-2 h-7 text-foreground/90 placeholder:text-muted-foreground bg-transparent", tag: { body: "h-7 relative bg-background border border-input hover:bg-background rounded-md font-medium text-xs ps-2 pe-7 flex", closeButton: @@ -172,11 +166,7 @@ export const YouTubeCrawlerView: FC = ({

{t("hint")}

- {error && ( -
- {error} -
- )} + {error &&
{error}
}

{t("tips_title")}

@@ -244,4 +234,3 @@ export const YouTubeCrawlerView: FC = ({
); }; - diff --git a/surfsense_web/components/assistant-ui/edit-composer.tsx b/surfsense_web/components/assistant-ui/edit-composer.tsx index 4e6346909..e2714661e 100644 --- a/surfsense_web/components/assistant-ui/edit-composer.tsx +++ b/surfsense_web/components/assistant-ui/edit-composer.tsx @@ -24,4 +24,3 @@ export const EditComposer: FC = () => { ); }; - diff --git a/surfsense_web/components/assistant-ui/thinking-steps.tsx b/surfsense_web/components/assistant-ui/thinking-steps.tsx index f0cf4a7c1..bbd4fca71 100644 --- a/surfsense_web/components/assistant-ui/thinking-steps.tsx +++ b/surfsense_web/components/assistant-ui/thinking-steps.tsx @@ -204,4 +204,3 @@ export const ThinkingStepsScrollHandler: FC = () => { return null; // This component doesn't render anything }; - diff --git a/surfsense_web/components/assistant-ui/thread-scroll-to-bottom.tsx b/surfsense_web/components/assistant-ui/thread-scroll-to-bottom.tsx index 6f641615e..79fee1850 100644 --- a/surfsense_web/components/assistant-ui/thread-scroll-to-bottom.tsx +++ b/surfsense_web/components/assistant-ui/thread-scroll-to-bottom.tsx @@ -16,4 +16,3 @@ export const ThreadScrollToBottom: FC = () => { ); }; - diff --git a/surfsense_web/components/assistant-ui/thread-welcome.tsx b/surfsense_web/components/assistant-ui/thread-welcome.tsx index b5e4bbac0..c101a5958 100644 --- a/surfsense_web/components/assistant-ui/thread-welcome.tsx +++ b/surfsense_web/components/assistant-ui/thread-welcome.tsx @@ -69,4 +69,3 @@ export const ThreadWelcome: FC = () => {
); }; - diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index 713a9af1c..ff61b8182 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -26,15 +26,7 @@ import { SquareIcon, } from "lucide-react"; import { useParams } from "next/navigation"; -import { - type FC, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import { type FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { mentionedDocumentIdsAtom, diff --git a/surfsense_web/components/assistant-ui/user-message.tsx b/surfsense_web/components/assistant-ui/user-message.tsx index fbbcf42bf..dcf626461 100644 --- a/surfsense_web/components/assistant-ui/user-message.tsx +++ b/surfsense_web/components/assistant-ui/user-message.tsx @@ -70,4 +70,3 @@ const UserActionBar: FC = () => { ); }; - diff --git a/surfsense_web/components/connectors/google-drive-folder-tree.tsx b/surfsense_web/components/connectors/google-drive-folder-tree.tsx index d15792bc8..8347d9cce 100644 --- a/surfsense_web/components/connectors/google-drive-folder-tree.tsx +++ b/surfsense_web/components/connectors/google-drive-folder-tree.tsx @@ -223,9 +223,17 @@ export function GoogleDriveFolderTree({ const childFiles = children?.filter((c) => !c.isFolder) || []; const indentSize = 0.75; // Smaller indent for mobile - + return ( -
+
renderItem(child, level + 1))} {children.length === 0 && ( -
Empty folder
+
+ Empty folder +
)}
)} diff --git a/surfsense_web/components/dashboard-breadcrumb.tsx b/surfsense_web/components/dashboard-breadcrumb.tsx index c4466ee4b..0e9374fdd 100644 --- a/surfsense_web/components/dashboard-breadcrumb.tsx +++ b/surfsense_web/components/dashboard-breadcrumb.tsx @@ -155,7 +155,6 @@ export function DashboardBreadcrumb() { return breadcrumbs; } - // Handle other sub-sections let subSectionLabel = subSection.charAt(0).toUpperCase() + subSection.slice(1); const subSectionLabels: Record = { diff --git a/surfsense_web/components/sidebar/app-sidebar.tsx b/surfsense_web/components/sidebar/app-sidebar.tsx index 6afe2dacb..d462b1feb 100644 --- a/surfsense_web/components/sidebar/app-sidebar.tsx +++ b/surfsense_web/components/sidebar/app-sidebar.tsx @@ -448,10 +448,7 @@ export const AppSidebar = memo(function AppSidebar({ - + = { RefreshCw, }; -export function NavChats({ - chats, - defaultOpen = true, - searchSpaceId, -}: NavChatsProps) { +export function NavChats({ chats, defaultOpen = true, searchSpaceId }: NavChatsProps) { const t = useTranslations("sidebar"); const router = useRouter(); const pathname = usePathname(); diff --git a/surfsense_web/components/sidebar/nav-notes.tsx b/surfsense_web/components/sidebar/nav-notes.tsx index ab1f3ff33..e9f94fe80 100644 --- a/surfsense_web/components/sidebar/nav-notes.tsx +++ b/surfsense_web/components/sidebar/nav-notes.tsx @@ -63,12 +63,7 @@ const actionIconMap: Record = { MoreHorizontal, }; -export function NavNotes({ - notes, - onAddNote, - defaultOpen = true, - searchSpaceId, -}: NavNotesProps) { +export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }: NavNotesProps) { const t = useTranslations("sidebar"); const router = useRouter(); const pathname = usePathname(); diff --git a/surfsense_web/contracts/enums/connectorIcons.tsx b/surfsense_web/contracts/enums/connectorIcons.tsx index fc509795b..22bc734aa 100644 --- a/surfsense_web/contracts/enums/connectorIcons.tsx +++ b/surfsense_web/contracts/enums/connectorIcons.tsx @@ -1,7 +1,4 @@ -import { - IconLinkPlus, - IconUsersGroup, -} from "@tabler/icons-react"; +import { IconLinkPlus, IconUsersGroup } from "@tabler/icons-react"; import { File, FileText, diff --git a/surfsense_web/hooks/use-connector-edit-page.ts b/surfsense_web/hooks/use-connector-edit-page.ts index f08642503..aa6354b19 100644 --- a/surfsense_web/hooks/use-connector-edit-page.ts +++ b/surfsense_web/hooks/use-connector-edit-page.ts @@ -166,12 +166,14 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) } } }, [ - connectorId, - connectors, - connectorsLoading, - router, - searchSpaceId, - connector, editForm.reset, patForm.reset + connectorId, + connectors, + connectorsLoading, + router, + searchSpaceId, + connector, + editForm.reset, + patForm.reset, // Note: editForm and patForm are intentionally excluded from dependencies // to prevent infinite loops. They are stable form objects from react-hook-form. ]); @@ -298,11 +300,15 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) } const candidateConfig: Record = { SEARXNG_HOST: host }; - const originalHost = typeof originalConfig.SEARXNG_HOST === "string" ? originalConfig.SEARXNG_HOST : ""; + const originalHost = + typeof originalConfig.SEARXNG_HOST === "string" ? originalConfig.SEARXNG_HOST : ""; let hasChanges = host !== originalHost.trim(); const apiKey = (formData.SEARXNG_API_KEY || "").trim(); - const originalApiKey = typeof originalConfig.SEARXNG_API_KEY === "string" ? originalConfig.SEARXNG_API_KEY : ""; + const originalApiKey = + typeof originalConfig.SEARXNG_API_KEY === "string" + ? originalConfig.SEARXNG_API_KEY + : ""; const originalApiKeyTrimmed = originalApiKey.trim(); if (apiKey !== originalApiKeyTrimmed) { candidateConfig.SEARXNG_API_KEY = apiKey || null; @@ -324,7 +330,10 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) } const language = (formData.SEARXNG_LANGUAGE || "").trim(); - const originalLanguage = typeof originalConfig.SEARXNG_LANGUAGE === "string" ? originalConfig.SEARXNG_LANGUAGE : ""; + const originalLanguage = + typeof originalConfig.SEARXNG_LANGUAGE === "string" + ? originalConfig.SEARXNG_LANGUAGE + : ""; const originalLanguageTrimmed = originalLanguage.trim(); if (language !== originalLanguageTrimmed) { candidateConfig.SEARXNG_LANGUAGE = language || null; @@ -534,13 +543,13 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) } try { - const updatedConnector = await updateConnector({ + const updatedConnector = (await updateConnector({ id: connectorId, data: { ...updatePayload, connector_type: connector.connector_type as EnumConnectorName, }, - }) as UpdateConnectorResponse; + })) as UpdateConnectorResponse; toast.success("Connector updated!"); // Use the response from the API which has the full merged config const newlySavedConfig = updatedConnector.config || originalConfig; diff --git a/surfsense_web/hooks/use-google-drive-folders.ts b/surfsense_web/hooks/use-google-drive-folders.ts index 65555a6c9..00a76327c 100644 --- a/surfsense_web/hooks/use-google-drive-folders.ts +++ b/surfsense_web/hooks/use-google-drive-folders.ts @@ -26,4 +26,3 @@ export function useGoogleDriveFolders({ retry: 2, }); } -