From 0e93d8420f2a97533557cf191d76ddeeb8279a2b Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 1 Jan 2026 15:11:00 +0530 Subject: [PATCH 01/12] fix: update connector links and streamline icon components - Changed Linkup API link from linkup.ai to linkup.so in the connector popup. - Removed unused IconSparkles and replaced it with Luma SVG in the connector icons. - Updated image properties for Linear, GitHub, Google Calendar, Google Gmail, and Google Drive connectors to ensure consistent sizing. - Replaced old SVG files for GitHub, Google Calendar, Google Drive, Google Gmail, and Linear with new versions for improved visuals. --- .../components/linkup-api-connect-form.tsx | 4 +- .../contracts/enums/connectorIcons.tsx | 27 +++-------- surfsense_web/public/connectors/github.svg | 2 +- .../public/connectors/google-calendar.svg | 45 +------------------ .../public/connectors/google-drive.svg | 45 +------------------ .../public/connectors/google-gmail.svg | 45 +------------------ surfsense_web/public/connectors/linear.svg | 6 ++- surfsense_web/public/connectors/luma.svg | 2 +- 8 files changed, 18 insertions(+), 158 deletions(-) diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linkup-api-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linkup-api-connect-form.tsx index dedb8c72e..ab0119e43 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linkup-api-connect-form.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linkup-api-connect-form.tsx @@ -79,12 +79,12 @@ export const LinkupApiConnectForm: FC = ({ You'll need a Linkup API key to use this connector. You can get one by signing up at{" "} - linkup.ai + linkup.so diff --git a/surfsense_web/contracts/enums/connectorIcons.tsx b/surfsense_web/contracts/enums/connectorIcons.tsx index d836701be..e37bea55e 100644 --- a/surfsense_web/contracts/enums/connectorIcons.tsx +++ b/surfsense_web/contracts/enums/connectorIcons.tsx @@ -1,6 +1,5 @@ import { IconLinkPlus, - IconSparkles, IconUsersGroup, } from "@tabler/icons-react"; import { @@ -19,28 +18,14 @@ import { EnumConnectorName } from "./connector"; export const getConnectorIcon = (connectorType: EnumConnectorName | string, className?: string) => { const iconProps = { className: className || "h-4 w-4" }; const imgProps = { className: className || "h-5 w-5", width: 20, height: 20 }; - // Larger props for specific services (Google services, GitHub, Linear) - scale up from size-6 to size-8 - const getLargeClassName = () => { - if (!className) return "h-8 w-8"; - // Replace size-6 with size-8, or h-6/w-6 with h-8/w-8 - return className - .replace(/size-6/g, "size-8") - .replace(/\bh-6\b/g, "h-8") - .replace(/\bw-6\b/g, "w-8"); - }; - const largeImgProps = { - className: getLargeClassName(), - width: 32, - height: 32 - }; switch (connectorType) { case EnumConnectorName.LINKUP_API: return ; case EnumConnectorName.LINEAR_CONNECTOR: - return Linear; + return Linear; case EnumConnectorName.GITHUB_CONNECTOR: - return GitHub; + return GitHub; case EnumConnectorName.TAVILY_API: return Tavily; case EnumConnectorName.SEARXNG_API: @@ -56,11 +41,11 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas case EnumConnectorName.JIRA_CONNECTOR: return Jira; case EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR: - return Google Calendar; + return Google Calendar; case EnumConnectorName.GOOGLE_GMAIL_CONNECTOR: - return Gmail; + return Gmail; case EnumConnectorName.GOOGLE_DRIVE_CONNECTOR: - return Google Drive; + return Google Drive; case EnumConnectorName.AIRTABLE_CONNECTOR: return Airtable; case EnumConnectorName.CONFLUENCE_CONNECTOR: @@ -70,7 +55,7 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas case EnumConnectorName.CLICKUP_CONNECTOR: return ClickUp; case EnumConnectorName.LUMA_CONNECTOR: - return ; + return Luma; case EnumConnectorName.ELASTICSEARCH_CONNECTOR: return Elasticsearch; case EnumConnectorName.WEBCRAWLER_CONNECTOR: diff --git a/surfsense_web/public/connectors/github.svg b/surfsense_web/public/connectors/github.svg index 63c462cc3..37fa923df 100644 --- a/surfsense_web/public/connectors/github.svg +++ b/surfsense_web/public/connectors/github.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/surfsense_web/public/connectors/google-calendar.svg b/surfsense_web/public/connectors/google-calendar.svg index f1f6f96c3..da764fe5b 100644 --- a/surfsense_web/public/connectors/google-calendar.svg +++ b/surfsense_web/public/connectors/google-calendar.svg @@ -1,44 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/surfsense_web/public/connectors/google-drive.svg b/surfsense_web/public/connectors/google-drive.svg index 35f214efd..67763ce23 100644 --- a/surfsense_web/public/connectors/google-drive.svg +++ b/surfsense_web/public/connectors/google-drive.svg @@ -1,44 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/surfsense_web/public/connectors/google-gmail.svg b/surfsense_web/public/connectors/google-gmail.svg index 47d9e973e..ed246b6ed 100644 --- a/surfsense_web/public/connectors/google-gmail.svg +++ b/surfsense_web/public/connectors/google-gmail.svg @@ -1,44 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/surfsense_web/public/connectors/linear.svg b/surfsense_web/public/connectors/linear.svg index 6252259bd..e2484d708 100644 --- a/surfsense_web/public/connectors/linear.svg +++ b/surfsense_web/public/connectors/linear.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + diff --git a/surfsense_web/public/connectors/luma.svg b/surfsense_web/public/connectors/luma.svg index 366f59ea8..13a0eeb33 100644 --- a/surfsense_web/public/connectors/luma.svg +++ b/surfsense_web/public/connectors/luma.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 543daa0434d69124e9b6f083e2f9109162e925ec Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 1 Jan 2026 20:38:12 +0530 Subject: [PATCH 02/12] refactor: streamline connector management UI and enhance document handling - Updated the ConnectorIndicator component to accurately reflect active connectors and their document counts. - Improved the display of standalone document types in the ActiveConnectorsTab, allowing users to view all documents easily. - Enhanced the ConnectorCard to show last indexed dates and formatted document counts for better clarity. - Adjusted tooltip and aria-labels for accessibility and consistency across attachment upload components. - Preserved newlines in URL input for webcrawler configuration to ensure proper backend handling. --- .../(manage)/components/DocumentTypeIcon.tsx | 2 - .../components/assistant-ui/attachment.tsx | 6 +- .../assistant-ui/composer-action.tsx | 84 ++++++----- .../assistant-ui/connector-popup.tsx | 11 +- .../components/connector-card.tsx | 38 +++-- .../components/webcrawler-config.tsx | 4 +- .../views/connector-edit-view.tsx | 68 +++++---- .../tabs/active-connectors-tab.tsx | 132 ++++++++++++++---- .../tabs/all-connectors-tab.tsx | 3 + .../contracts/enums/connectorIcons.tsx | 4 +- surfsense_web/public/connectors/bookstack.svg | 2 +- surfsense_web/public/connectors/searxng.svg | 1 + 12 files changed, 239 insertions(+), 116 deletions(-) create mode 100644 surfsense_web/public/connectors/searxng.svg diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx index 99d7a7b8d..e483dea12 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx @@ -3,8 +3,6 @@ import type React from "react"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; -type IconComponent = React.ComponentType<{ size?: number; className?: string }>; - export function getDocumentTypeIcon(type: string): React.ReactNode { return getConnectorIcon(type); } diff --git a/surfsense_web/components/assistant-ui/attachment.tsx b/surfsense_web/components/assistant-ui/attachment.tsx index c08736d7c..74d0c0d63 100644 --- a/surfsense_web/components/assistant-ui/attachment.tsx +++ b/surfsense_web/components/assistant-ui/attachment.tsx @@ -337,12 +337,12 @@ export const ComposerAddAttachment: FC = () => { @@ -350,7 +350,7 @@ export const ComposerAddAttachment: FC = () => { - Add attachment(s) + Add attachment diff --git a/surfsense_web/components/assistant-ui/composer-action.tsx b/surfsense_web/components/assistant-ui/composer-action.tsx index ba27f40c2..4692ddd82 100644 --- a/surfsense_web/components/assistant-ui/composer-action.tsx +++ b/surfsense_web/components/assistant-ui/composer-action.tsx @@ -38,11 +38,10 @@ const ConnectorIndicator: FC = () => { ? Object.entries(documentTypeCounts).filter(([_, count]) => count > 0) : []; - const nonIndexableConnectors = connectors.filter((connector) => !connector.is_indexable); - - const hasConnectors = nonIndexableConnectors.length > 0; + // Count only active connectors (matching what's shown in the Active tab) + const activeConnectorsCount = connectors.length; + const hasConnectors = activeConnectorsCount > 0; const hasSources = hasConnectors || activeDocumentTypes.length > 0; - const totalSourceCount = nonIndexableConnectors.length + activeDocumentTypes.length; const handleMouseEnter = useCallback(() => { // Clear any pending close timeout @@ -76,7 +75,7 @@ const ConnectorIndicator: FC = () => { "text-muted-foreground" )} aria-label={ - hasSources ? `View ${totalSourceCount} connected sources` : "Add your first connector" + hasConnectors ? `View ${activeConnectorsCount} active connectors` : "Add your first connector" } onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} @@ -86,9 +85,9 @@ const ConnectorIndicator: FC = () => { ) : ( <> - {totalSourceCount > 0 && ( + {activeConnectorsCount > 0 && ( - {totalSourceCount > 99 ? "99+" : totalSourceCount} + {activeConnectorsCount > 99 ? "99+" : activeConnectorsCount} )} @@ -104,35 +103,50 @@ const ConnectorIndicator: FC = () => { > {hasSources ? (
-
-

Connected Sources

- - {totalSourceCount} - -
-
- {activeDocumentTypes.map(([docType, count]) => ( -
- {getConnectorIcon(docType, "size-3.5")} - {getDocumentTypeLabel(docType)} - - {count > 999 ? "999+" : count} - + {activeConnectorsCount > 0 && ( +
+

Active Connectors

+ + {activeConnectorsCount} + +
+ )} + {activeConnectorsCount > 0 && ( +
+ {connectors.map((connector) => ( +
+ {getConnectorIcon(connector.connector_type, "size-3.5")} + {connector.name} +
+ ))} +
+ )} + {activeDocumentTypes.length > 0 && ( + <> + {activeConnectorsCount > 0 && ( +
+

Documents

+
+ )} +
+ {activeDocumentTypes.map(([docType, count]) => ( +
+ {getConnectorIcon(docType, "size-3.5")} + {getDocumentTypeLabel(docType)} + + {count > 999 ? "999+" : count} + +
+ ))}
- ))} - {nonIndexableConnectors.map((connector) => ( -
- {getConnectorIcon(connector.connector_type, "size-3.5")} - {connector.name} -
- ))} -
+ + )}
{ const hasConnectors = connectors.length > 0; const hasSources = hasConnectors || activeDocumentTypes.length > 0; const totalSourceCount = connectors.length + activeDocumentTypes.length; + const activeConnectorsCount = connectors.length; // Only actual connectors, not document types // Check which connectors are already connected const connectedTypes = new Set( @@ -170,7 +171,7 @@ export const ConnectorIndicator: FC = () => { return ( { "border-0 ring-0 focus:ring-0 shadow-none focus:shadow-none" )} aria-label={ - hasSources ? `View ${totalSourceCount} connected sources` : "Add your first connector" + hasConnectors ? `View ${activeConnectorsCount} connectors` : "Add your first connector" } onClick={() => handleOpenChange(true)} > @@ -188,9 +189,9 @@ export const ConnectorIndicator: FC = () => { ) : ( <> - {totalSourceCount > 0 && ( + {activeConnectorsCount > 0 && ( - {totalSourceCount > 99 ? "99+" : totalSourceCount} + {activeConnectorsCount > 99 ? "99+" : activeConnectorsCount} )} @@ -259,7 +260,7 @@ export const ConnectorIndicator: FC = () => { {/* Header */} void; @@ -33,6 +35,20 @@ function extractIndexedCount(message: string | undefined): number | null { return match ? parseInt(match[1], 10) : null; } +/** + * Format document count (e.g., "1.2k docs", "500 docs", "1.5M docs") + */ +function formatDocumentCount(count: number | undefined): string { + if (count === undefined || count === 0) return "0 docs"; + if (count < 1000) return `${count} docs`; + if (count < 1000000) { + const k = (count / 1000).toFixed(1); + return `${k.replace(/\.0$/, "")}k docs`; + } + const m = (count / 1000000).toFixed(1); + return `${m.replace(/\.0$/, "")}M docs`; +} + export const ConnectorCard: FC = ({ id, title, @@ -41,6 +57,7 @@ export const ConnectorCard: FC = ({ isConnected = false, isConnecting = false, documentCount, + lastIndexedAt, isIndexing = false, activeTask, onConnect, @@ -70,18 +87,16 @@ export const ConnectorCard: FC = ({ } if (isConnected) { - if (documentCount !== undefined && documentCount > 0) { + // Show last indexed date for connected connectors + if (lastIndexedAt) { return ( - - - - {documentCount.toLocaleString()} document{documentCount !== 1 ? "s" : ""} - + + Last indexed: {format(new Date(lastIndexedAt), "MMM d, yyyy")} ); } - // Fallback for connected but no documents yet - return No documents indexed; + // Fallback for connected but never indexed + return Never indexed; } return description; @@ -105,6 +120,11 @@ export const ConnectorCard: FC = ({
{getStatusContent()}
+ {isConnected && documentCount !== undefined && ( +

+ {formatDocumentCount(documentCount)} +

+ )}
{/* Fixed Footer - Action buttons */} -
+
{showDisconnectConfirm ? ( -
- Are you sure? - - +
+ Are you sure? +
+ + +
) : ( )} -
); })} -
+
+
+ )} + + {/* Standalone Documents Section */} + {standaloneDocuments.length > 0 && ( +
+
+

+ Documents +

+ +
+
+ {standaloneDocuments.map((doc) => ( +
+
+ {getConnectorIcon(doc.type, "size-3.5")} +
+ + {doc.label} + + + {formatDocumentCount(doc.count)} + +
+ ))} +
+
+ )} ) : (
diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx index 7ea9035c4..193e3adee 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx @@ -101,6 +101,7 @@ export const AllConnectorsTab: FC = ({ isConnected={isConnected} isConnecting={isConnecting} documentCount={documentCount} + lastIndexedAt={actualConnector?.last_indexed_at} isIndexing={isIndexing} activeTask={activeTask} onConnect={() => onConnectOAuth(connector)} @@ -162,6 +163,7 @@ export const AllConnectorsTab: FC = ({ isConnected={isConnected} isConnecting={isConnecting} documentCount={documentCount} + lastIndexedAt={actualConnector?.last_indexed_at} isIndexing={isIndexing} activeTask={activeTask} onConnect={handleConnect} @@ -230,6 +232,7 @@ export const AllConnectorsTab: FC = ({ isConnected={isConnected} isConnecting={isConnecting} documentCount={documentCount} + lastIndexedAt={actualConnector?.last_indexed_at} isIndexing={isIndexing} activeTask={activeTask} onConnect={handleConnect} diff --git a/surfsense_web/contracts/enums/connectorIcons.tsx b/surfsense_web/contracts/enums/connectorIcons.tsx index e37bea55e..fc509795b 100644 --- a/surfsense_web/contracts/enums/connectorIcons.tsx +++ b/surfsense_web/contracts/enums/connectorIcons.tsx @@ -29,7 +29,7 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas case EnumConnectorName.TAVILY_API: return Tavily; case EnumConnectorName.SEARXNG_API: - return ; + return SearXNG; case EnumConnectorName.BAIDU_SEARCH_API: return Baidu; case EnumConnectorName.SLACK_CONNECTOR: @@ -81,6 +81,8 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas return Zoom; case "FILE": return ; + case "GOOGLE_DRIVE_FILE": + return ; case "NOTE": return ; case "EXTENSION": diff --git a/surfsense_web/public/connectors/bookstack.svg b/surfsense_web/public/connectors/bookstack.svg index 8b7829055..c97639b74 100644 --- a/surfsense_web/public/connectors/bookstack.svg +++ b/surfsense_web/public/connectors/bookstack.svg @@ -1 +1 @@ -BookStack \ No newline at end of file + \ No newline at end of file diff --git a/surfsense_web/public/connectors/searxng.svg b/surfsense_web/public/connectors/searxng.svg new file mode 100644 index 000000000..a5e210e20 --- /dev/null +++ b/surfsense_web/public/connectors/searxng.svg @@ -0,0 +1 @@ + \ No newline at end of file From b909032e32229a69b76a58f14588ea3a5c527fae Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 1 Jan 2026 21:22:34 +0530 Subject: [PATCH 03/12] feat: enhance Google Drive connector functionality and UI - Added support for selecting both folders and files in the Google Drive connector configuration. - Updated the UI to reflect the selection of files alongside folders, improving user clarity. - Introduced a quick indexing feature for connectors, allowing users to start indexing without date selection. - Adjusted periodic sync settings to be disabled for Google Drive connectors, ensuring proper functionality. - Improved styling and accessibility across various components in the connector popup. --- .../assistant-ui/connector-popup.tsx | 3 + .../components/periodic-sync-config.tsx | 2 +- .../components/google-drive-config.tsx | 48 +++++++++--- .../views/connector-edit-view.tsx | 67 +++++++++++----- .../views/indexing-configuration-view.tsx | 15 ++-- .../hooks/use-connector-dialog.ts | 77 ++++++++++++++----- .../connectors/google-drive-folder-tree.tsx | 16 +--- 7 files changed, 160 insertions(+), 68 deletions(-) diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 1e7150359..a83f32a09 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -91,6 +91,7 @@ export const ConnectorIndicator: FC = () => { handleBackFromEdit, handleBackFromConnect, handleBackFromYouTube, + handleQuickIndexConnector, connectorConfig, setConnectorConfig, setIndexingConnectorConfig, @@ -225,6 +226,7 @@ export const ConnectorIndicator: FC = () => { frequencyMinutes={frequencyMinutes} isSaving={isSaving} isDisconnecting={isDisconnecting} + isIndexing={indexingConnectorIds.has(editingConnector.id)} onStartDateChange={setStartDate} onEndDateChange={setEndDate} onPeriodicEnabledChange={setPeriodicEnabled} @@ -232,6 +234,7 @@ export const ConnectorIndicator: FC = () => { onSave={() => handleSaveConnector(() => refreshConnectors())} onDisconnect={() => handleDisconnectConnector(() => refreshConnectors())} onBack={handleBackFromEdit} + onQuickIndex={editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" ? () => handleQuickIndexConnector(editingConnector.id) : undefined} onConfigChange={setConnectorConfig} onNameChange={setConnectorName} /> diff --git a/surfsense_web/components/assistant-ui/connector-popup/components/periodic-sync-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/components/periodic-sync-config.tsx index 1d22e6890..ee2792204 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/components/periodic-sync-config.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/components/periodic-sync-config.tsx @@ -37,7 +37,7 @@ export const PeriodicSyncConfig: FC = ({
{enabled && ( -
+
- - - - - Every 15 minutes - Every hour - Every 6 hours - Every 12 hours - Daily (24 hours) - Weekly (7 days) - Custom - - -
- - {frequencyMinutes === "custom" && ( -
- - setCustomFrequency(e.target.value)} - /> -

- Enter the number of minutes between each indexing run -

-
- )} - -
-

Preview:

-

- {frequencyMinutes === "custom" && customFrequency - ? `Will run every ${customFrequency} minutes` - : frequencyMinutes === "15" - ? "Will run every 15 minutes" - : frequencyMinutes === "60" - ? "Will run every hour" - : frequencyMinutes === "360" - ? "Will run every 6 hours" - : frequencyMinutes === "720" - ? "Will run every 12 hours" - : frequencyMinutes === "1440" - ? "Will run daily (every 24 hours)" - : frequencyMinutes === "10080" - ? "Will run weekly (every 7 days)" - : "Select a frequency above"} -

-
-
- )} -
- - - - - - - - ); -} diff --git a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx index a44592ab2..afaa3abef 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx @@ -26,28 +26,24 @@ export default function DashboardLayout({ }, ]; - const customNavMain = [ - { - title: "Chat", - url: `/dashboard/${search_space_id}/new-chat`, - icon: "SquareTerminal", - items: [], - }, - { - title: "Sources", - url: "#", - icon: "Database", - items: [ - { - title: "Manage Documents", - url: `/dashboard/${search_space_id}/documents`, - }, - { - title: "Manage Connectors", - url: `/dashboard/${search_space_id}/connectors`, - }, - ], - }, + const customNavMain = [ + { + title: "Chat", + url: `/dashboard/${search_space_id}/new-chat`, + icon: "SquareTerminal", + items: [], + }, + { + title: "Sources", + url: "#", + icon: "Database", + items: [ + { + title: "Manage Documents", + url: `/dashboard/${search_space_id}/documents`, + }, + ], + }, { title: "Logs", url: `/dashboard/${search_space_id}/logs`, diff --git a/surfsense_web/components/assistant-ui/composer-action.tsx b/surfsense_web/components/assistant-ui/composer-action.tsx index 4692ddd82..d359342d1 100644 --- a/surfsense_web/components/assistant-ui/composer-action.tsx +++ b/surfsense_web/components/assistant-ui/composer-action.tsx @@ -1,7 +1,6 @@ import { AssistantIf, ComposerPrimitive, useAssistantState } from "@assistant-ui/react"; import { useAtomValue } from "jotai"; import { AlertCircle, ArrowUpIcon, Loader2, Plus, Plug2, SquareIcon } from "lucide-react"; -import Link from "next/link"; import type { FC } from "react"; import { useCallback, useMemo, useRef, useState } from "react"; import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; @@ -148,14 +147,15 @@ const ConnectorIndicator: FC = () => { )}
- {/* Connector popup should be opened via the connector indicator button */}} > Add more sources - +
) : ( @@ -164,13 +164,14 @@ const ConnectorIndicator: FC = () => {

Add documents or connect data sources to enhance search results.

- {/* Connector popup should be opened via the connector indicator button */}} > Add Connector - + )} 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 abfdab575..97f23d126 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 @@ -688,29 +688,7 @@ export const useConnectorDialog = () => { const handleStartEdit = useCallback((connector: SearchSourceConnector) => { if (!searchSpaceId) return; - // Check if this is an OAuth connector - const isOAuthConnector = OAUTH_CONNECTORS.some( - (oauthConnector) => oauthConnector.connectorType === connector.connector_type - ); - - // Check if this is webcrawler, Tavily API, SearxNG, Linkup, Baidu, Linear, Elasticsearch, Slack, Discord, or Notion (can be managed in popup) - const isWebcrawler = connector.connector_type === EnumConnectorName.WEBCRAWLER_CONNECTOR; - const isTavilyApi = connector.connector_type === EnumConnectorName.TAVILY_API; - const isSearxng = connector.connector_type === EnumConnectorName.SEARXNG_API; - const isLinkup = connector.connector_type === EnumConnectorName.LINKUP_API; - const isBaidu = connector.connector_type === EnumConnectorName.BAIDU_SEARCH_API; - const isLinear = connector.connector_type === EnumConnectorName.LINEAR_CONNECTOR; - const isElasticsearch = connector.connector_type === EnumConnectorName.ELASTICSEARCH_CONNECTOR; - const isSlack = connector.connector_type === EnumConnectorName.SLACK_CONNECTOR; - const isDiscord = connector.connector_type === EnumConnectorName.DISCORD_CONNECTOR; - const isNotion = connector.connector_type === EnumConnectorName.NOTION_CONNECTOR; - - // If not OAuth, not webcrawler, not Tavily API, not SearxNG, not Linkup, not Baidu, not Linear, not Elasticsearch, not Slack, not Discord, and not Notion, redirect to old connector edit page - if (!isOAuthConnector && !isWebcrawler && !isTavilyApi && !isSearxng && !isLinkup && !isBaidu && !isLinear && !isElasticsearch && !isSlack && !isDiscord && !isNotion) { - router.push(`/dashboard/${searchSpaceId}/connectors/${connector.id}/edit`); - return; - } - + // All connector types should be handled in the popup edit view // Validate connector data const connectorValidation = searchSourceConnector.safeParse(connector); if (!connectorValidation.success) { @@ -733,7 +711,7 @@ export const useConnectorDialog = () => { url.searchParams.set("view", "edit"); url.searchParams.set("connectorId", connector.id.toString()); window.history.pushState({ modal: true }, "", url.toString()); - }, [searchSpaceId, router]); + }, [searchSpaceId]); // Handle saving connector changes const handleSaveConnector = useCallback(async (refreshConnectors: () => void) => { diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx index 193e3adee..1f955dc2d 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx @@ -1,7 +1,6 @@ "use client"; -import { useRouter } from "next/navigation"; -import { type FC } from "react"; +import type { FC } from "react"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types"; import { OAUTH_CONNECTORS, CRAWLERS, OTHER_CONNECTORS } from "../constants/connector-constants"; @@ -39,8 +38,6 @@ export const AllConnectorsTab: FC = ({ onCreateYouTubeCrawler, onManage, }) => { - const router = useRouter(); - // Helper to find active task for a connector const getActiveTaskForConnector = (connectorId: number): LogActiveTask | undefined => { if (!logsSummary?.active_tasks) return undefined; @@ -148,9 +145,11 @@ export const AllConnectorsTab: FC = ({ : isWebcrawler && onCreateWebcrawler ? onCreateWebcrawler : crawler.connectorType && onConnectNonOAuth - ? () => onConnectNonOAuth(crawler.connectorType!) - : crawler.connectorType - ? () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${crawler.id}`) + ? () => { + if (crawler.connectorType) { + onConnectNonOAuth(crawler.connectorType); + } + } : () => {}; // Fallback for non-connector crawlers return ( @@ -186,7 +185,6 @@ export const AllConnectorsTab: FC = ({
{filteredOther.map((connector) => { // Special handling for connectors that can be created in popup - const isWebcrawler = connector.id === "webcrawler-connector"; const isTavily = connector.id === "tavily-api"; const isSearxng = connector.id === "searxng"; const isLinkup = connector.id === "linkup-api"; @@ -216,11 +214,9 @@ export const AllConnectorsTab: FC = ({ const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id); const activeTask = actualConnector ? getActiveTaskForConnector(actualConnector.id) : undefined; - const handleConnect = isWebcrawler && onCreateWebcrawler - ? onCreateWebcrawler - : (isTavily || isSearxng || isLinkup || isBaidu || isLinear || isElasticsearch || isSlack || isDiscord || isNotion || isConfluence || isBookStack || isGithub || isJira || isClickUp || isLuma || isCircleback) && onConnectNonOAuth + const handleConnect = (isTavily || isSearxng || isLinkup || isBaidu || isLinear || isElasticsearch || isSlack || isDiscord || isNotion || isConfluence || isBookStack || isGithub || isJira || isClickUp || isLuma || isCircleback) && onConnectNonOAuth ? () => onConnectNonOAuth(connector.connectorType) - : () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`); + : () => {}; // Fallback - connector popup should handle all connector types return ( = { "new-chat": t("chat") || "Chat", documents: t("documents"), - connectors: t("connectors"), logs: t("logs"), settings: t("settings"), editor: t("editor"), @@ -156,53 +155,6 @@ export function DashboardBreadcrumb() { return breadcrumbs; } - // Handle connector sub-sections - if (section === "connectors") { - // Handle specific connector types - if (subSection === "add" && segments[4]) { - const connectorType = segments[4]; - const connectorLabels: Record = { - "github-connector": "GitHub", - "jira-connector": "Jira", - "confluence-connector": "Confluence", - "bookstack-connector": "BookStack", - "discord-connector": "Discord", - "linear-connector": "Linear", - "clickup-connector": "ClickUp", - "slack-connector": "Slack", - "notion-connector": "Notion", - "tavily-api": "Tavily API", - "linkup-api": "LinkUp API", - "luma-connector": "Luma", - "elasticsearch-connector": "Elasticsearch", - }; - - const connectorLabel = connectorLabels[connectorType] || connectorType; - breadcrumbs.push({ - label: "Connectors", - href: `/dashboard/${segments[1]}/connectors`, - }); - breadcrumbs.push({ - label: "Add Connector", - href: `/dashboard/${segments[1]}/connectors/add`, - }); - breadcrumbs.push({ label: connectorLabel }); - return breadcrumbs; - } - - const connectorLabels: Record = { - add: t("add_connector"), - manage: t("manage_connectors"), - }; - - const connectorLabel = connectorLabels[subSection] || subSection; - breadcrumbs.push({ - label: t("connectors"), - href: `/dashboard/${segments[1]}/connectors`, - }); - breadcrumbs.push({ label: connectorLabel }); - return breadcrumbs; - } // Handle other sub-sections let subSectionLabel = subSection.charAt(0).toUpperCase() + subSection.slice(1); @@ -210,8 +162,6 @@ export function DashboardBreadcrumb() { upload: t("upload_documents"), youtube: t("add_youtube"), webpage: t("add_webpages"), - add: t("add_connector"), - edit: t("edit_connector"), manage: t("manage"), }; diff --git a/surfsense_web/hooks/use-connector-edit-page.ts b/surfsense_web/hooks/use-connector-edit-page.ts index 05f5abcc2..f08642503 100644 --- a/surfsense_web/hooks/use-connector-edit-page.ts +++ b/surfsense_web/hooks/use-connector-edit-page.ts @@ -16,6 +16,7 @@ import { } from "@/components/editConnector/types"; import type { EnumConnectorName } from "@/contracts/enums/connector"; import type { SearchSourceConnector } from "@/hooks/use-search-source-connectors"; +import type { UpdateConnectorResponse } from "@/contracts/types/connector.types"; import { authenticatedFetch } from "@/lib/auth-utils"; const normalizeListInput = (value: unknown): string[] => { @@ -57,7 +58,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) // State managed by the hook const [connector, setConnector] = useState(null); - const [originalConfig, setOriginalConfig] = useState | null>(null); + const [originalConfig, setOriginalConfig] = useState | null>(null); const [isSaving, setIsSaving] = useState(false); const [currentSelectedRepos, setCurrentSelectedRepos] = useState([]); const [originalPat, setOriginalPat] = useState(""); @@ -161,18 +162,18 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) } } else { toast.error("Connector not found."); - router.push(`/dashboard/${searchSpaceId}/connectors`); + router.push(`/dashboard/${searchSpaceId}`); } } }, [ - connectorId, - connectors, - connectorsLoading, - router, - searchSpaceId, - connector, - editForm, - patForm, + 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. ]); // Handlers managed by the hook @@ -219,7 +220,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) setIsSaving(true); const updatePayload: Partial = {}; let configChanged = false; - let newConfig: Record | null = null; + let newConfig: Record | null = null; if (formData.name !== connector.name) { updatePayload.name = formData.name; @@ -296,12 +297,14 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) return; } - const candidateConfig: Record = { SEARXNG_HOST: host }; - let hasChanges = host !== (originalConfig.SEARXNG_HOST || "").trim(); + const candidateConfig: Record = { SEARXNG_HOST: 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 = (originalConfig.SEARXNG_API_KEY || "").trim(); - if (apiKey !== originalApiKey) { + 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; hasChanges = true; } @@ -321,8 +324,9 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) } const language = (formData.SEARXNG_LANGUAGE || "").trim(); - const originalLanguage = (originalConfig.SEARXNG_LANGUAGE || "").trim(); - if (language !== originalLanguage) { + const originalLanguage = typeof originalConfig.SEARXNG_LANGUAGE === "string" ? originalConfig.SEARXNG_LANGUAGE : ""; + const originalLanguageTrimmed = originalLanguage.trim(); + if (language !== originalLanguageTrimmed) { candidateConfig.SEARXNG_LANGUAGE = language || null; hasChanges = true; } @@ -490,7 +494,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) ) { newConfig = {}; - if (formData.FIRECRAWL_API_KEY && formData.FIRECRAWL_API_KEY.trim()) { + if (formData.FIRECRAWL_API_KEY?.trim()) { if (!formData.FIRECRAWL_API_KEY.startsWith("fc-")) { toast.warning( "Firecrawl API keys typically start with 'fc-'. Please verify your key." @@ -504,7 +508,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) } if (formData.INITIAL_URLS !== undefined) { - if (formData.INITIAL_URLS && formData.INITIAL_URLS.trim()) { + if (formData.INITIAL_URLS?.trim()) { newConfig.INITIAL_URLS = formData.INITIAL_URLS.trim(); } else if (originalConfig.INITIAL_URLS) { toast.info("URLs removed from crawler configuration."); @@ -530,21 +534,19 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string) } try { - await updateConnector({ + const updatedConnector = await updateConnector({ id: connectorId, data: { ...updatePayload, connector_type: connector.connector_type as EnumConnectorName, }, - }); + }) as UpdateConnectorResponse; toast.success("Connector updated!"); - const newlySavedConfig = updatePayload.config || originalConfig; + // Use the response from the API which has the full merged config + const newlySavedConfig = updatedConnector.config || originalConfig; setOriginalConfig(newlySavedConfig); - if (updatePayload.name) { - setConnector((prev) => - prev ? { ...prev, name: updatePayload.name!, config: newlySavedConfig } : null - ); - } + // Update connector state with the full updated connector from the API + setConnector(updatedConnector); if (configChanged) { if (connector.connector_type === "GITHUB_CONNECTOR") { const savedGitHubConfig = newlySavedConfig as { From 5f7684499230d33f0b8a3419ab306efc6609529d Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 1 Jan 2026 22:08:20 +0530 Subject: [PATCH 05/12] refactor: update dashboard layout and enhance Circleback connector configuration - Modified the DashboardLayout to replace the "Sources" section with a "Documents" entry, improving navigation clarity. - Updated the CirclebackConfig component to include an Info icon for configuration instructions and adjusted the webhook URL input to be disabled for better user experience. - Enhanced the useConnectorDialog hook to handle non-indexable connectors more effectively, ensuring proper state management and user feedback. - Improved icon mapping in the app sidebar for consistency across components. --- .../dashboard/[search_space_id]/layout.tsx | 21 ++--- .../components/circleback-config.tsx | 9 ++- .../hooks/use-connector-dialog.ts | 77 ++++++++++++++----- .../components/sidebar/app-sidebar.tsx | 4 + 4 files changed, 76 insertions(+), 35 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx index afaa3abef..ae3c5ad9c 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx @@ -30,24 +30,19 @@ export default function DashboardLayout({ { title: "Chat", url: `/dashboard/${search_space_id}/new-chat`, - icon: "SquareTerminal", + icon: "MessageCircle", items: [], }, - { - title: "Sources", - url: "#", - icon: "Database", - items: [ - { - title: "Manage Documents", - url: `/dashboard/${search_space_id}/documents`, - }, - ], - }, + { + title: "Documents", + url: `/dashboard/${search_space_id}/documents`, + icon: "SquareLibrary", + items: [], + }, { title: "Logs", url: `/dashboard/${search_space_id}/logs`, - icon: "FileText", + icon: "Logs", items: [], }, ]; 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 48e4b43e5..26c070b0b 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 @@ -1,6 +1,6 @@ "use client"; -import { Copy, Webhook, Check } from "lucide-react"; +import { Copy, Webhook, Check, Info } from "lucide-react"; import { useState, useEffect } from "react"; import type { FC } from "react"; import { Input } from "@/components/ui/input"; @@ -107,11 +107,12 @@ export const CirclebackConfig: FC = ({ +
+

{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, }); } - From 3227c6c04305bb36d08db586791f3d089ee960bc Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 1 Jan 2026 23:24:34 +0530 Subject: [PATCH 08/12] refactor: enhance styling and layout of connector edit view - Adjusted padding and height properties for action buttons in the connector edit view to improve UI consistency and usability. - Ensured that button sizes are uniform across different screen sizes for a better user experience. --- .../connector-configs/views/connector-edit-view.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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 d6c1faf98..e57861d55 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 @@ -239,7 +239,7 @@ export const ConnectorEditView: FC = ({
{/* Fixed Footer - Action buttons */} -
+
{showDisconnectConfirm ? (
@@ -251,7 +251,7 @@ export const ConnectorEditView: FC = ({ size="sm" onClick={handleDisconnectConfirm} disabled={isDisconnecting} - className="text-xs sm:text-sm flex-1 sm:flex-initial" + className="text-xs sm:text-sm flex-1 sm:flex-initial h-10 sm:h-auto py-2 sm:py-2" > {isDisconnecting ? ( <> @@ -267,7 +267,7 @@ export const ConnectorEditView: FC = ({ size="sm" onClick={handleDisconnectCancel} disabled={isDisconnecting} - className="text-xs sm:text-sm flex-1 sm:flex-initial" + className="text-xs sm:text-sm flex-1 sm:flex-initial h-10 sm:h-auto py-2 sm:py-2" > Cancel @@ -276,10 +276,9 @@ export const ConnectorEditView: FC = ({ ) : (
)} - - {/* More Integrations */} - {filteredOther.length > 0 && ( -
-
-

More Integrations

-
-
- {filteredOther.map((connector) => { - // Special handling for connectors that can be created in popup - const isTavily = connector.id === "tavily-api"; - const isSearxng = connector.id === "searxng"; - const isLinkup = connector.id === "linkup-api"; - const isBaidu = connector.id === "baidu-search-api"; - const isLinear = connector.id === "linear-connector"; - const isElasticsearch = connector.id === "elasticsearch-connector"; - const isSlack = connector.id === "slack-connector"; - const isDiscord = connector.id === "discord-connector"; - const isNotion = connector.id === "notion-connector"; - const isConfluence = connector.id === "confluence-connector"; - const isBookStack = connector.id === "bookstack-connector"; - const isGithub = connector.id === "github-connector"; - const isJira = connector.id === "jira-connector"; - const isClickUp = connector.id === "clickup-connector"; - const isLuma = connector.id === "luma-connector"; - const isCircleback = connector.id === "circleback-connector"; - - const isConnected = connectedTypes.has(connector.connectorType); - const isConnecting = connectingId === connector.id; - - // Find the actual connector object if connected - const actualConnector = - isConnected && allConnectors - ? allConnectors.find( - (c: SearchSourceConnector) => c.connector_type === connector.connectorType - ) - : undefined; - - const documentCount = getDocumentCountForConnector( - connector.connectorType, - documentTypeCounts - ); - const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id); - const activeTask = actualConnector - ? getActiveTaskForConnector(actualConnector.id) - : undefined; - - const handleConnect = - (isTavily || - isSearxng || - isLinkup || - isBaidu || - isLinear || - isElasticsearch || - isSlack || - isDiscord || - isNotion || - isConfluence || - isBookStack || - isGithub || - isJira || - isClickUp || - isLuma || - isCircleback) && - onConnectNonOAuth - ? () => onConnectNonOAuth(connector.connectorType) - : () => {}; // Fallback - connector popup should handle all connector types - - return ( - onManage(actualConnector) : undefined - } - /> - ); - })} -
-
- )}
); }; diff --git a/surfsense_web/components/sidebar/app-sidebar.tsx b/surfsense_web/components/sidebar/app-sidebar.tsx index d462b1feb..fb3bf3022 100644 --- a/surfsense_web/components/sidebar/app-sidebar.tsx +++ b/surfsense_web/components/sidebar/app-sidebar.tsx @@ -3,6 +3,7 @@ import { useAtomValue } from "jotai"; import { AlertCircle, + ArrowLeftRight, BookOpen, Cable, ChevronsUpDown, @@ -417,7 +418,7 @@ export const AppSidebar = memo(function AppSidebar({ )} router.push("/dashboard")}> - + Switch workspace diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json index 151556d3a..fd655be6c 100644 --- a/surfsense_web/messages/en.json +++ b/surfsense_web/messages/en.json @@ -374,7 +374,7 @@ "tip_4": "Processing may take some time depending on video length", "preview": "Preview", "cancel": "Cancel", - "submit": "Submit YouTube Videos", + "submit": "Add", "processing": "Processing...", "error_no_video": "Please add at least one YouTube video URL", "error_invalid_urls": "Invalid YouTube URLs detected: {urls}", From aa96e08231cc4e7ccdaf16e14b621566da92f5b8 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 2 Jan 2026 01:23:04 +0530 Subject: [PATCH 10/12] feat: implement search functionality in connector popup - Added a search input in the ConnectorDialogHeader to filter active connectors based on user input. - Enhanced the ActiveConnectorsTab to filter displayed connectors and document types according to the search query. - Introduced a clear search button for improved user experience when managing connectors. --- .../assistant-ui/connector-popup.tsx | 1 + .../components/connector-dialog-header.tsx | 17 ++++++++++++-- .../tabs/active-connectors-tab.tsx | 22 ++++++++++++++++--- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 5560a3c04..621861529 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -292,6 +292,7 @@ export const ConnectorIndicator: FC = () => { = ({ onSearchChange(e.target.value)} /> + {searchQuery && ( + + )}
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 c17afc84f..2c50530ce 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 @@ -14,6 +14,7 @@ import { getDocumentCountForConnector } from "../utils/connector-document-mappin import { TabsContent } from "@/components/ui/tabs"; interface ActiveConnectorsTabProps { + searchQuery: string; hasSources: boolean; totalSourceCount: number; activeDocumentTypes: Array<[string, number]>; @@ -26,6 +27,7 @@ interface ActiveConnectorsTabProps { } export const ActiveConnectorsTab: FC = ({ + searchQuery, hasSources, activeDocumentTypes, connectors, @@ -74,20 +76,34 @@ export const ActiveConnectorsTab: FC = ({ type: docType, count, label: getDocumentTypeLabel(docType), - })); + })) + .filter((doc) => { + if (!searchQuery) return true; + return doc.label.toLowerCase().includes(searchQuery.toLowerCase()); + }); + + // Filter connectors based on search query + const filteredConnectors = connectors.filter((connector) => { + if (!searchQuery) return true; + const searchLower = searchQuery.toLowerCase(); + return ( + connector.name.toLowerCase().includes(searchLower) || + connector.connector_type.toLowerCase().includes(searchLower) + ); + }); return ( {hasSources ? (
{/* Active Connectors Section */} - {connectors.length > 0 && ( + {filteredConnectors.length > 0 && (

Active Connectors

- {connectors.map((connector) => { + {filteredConnectors.map((connector) => { const isIndexing = indexingConnectorIds.has(connector.id); const activeTask = logsSummary?.active_tasks?.find( (task: LogActiveTask) => task.connector_id === connector.id From 5ebb9d7aeac22be9410d104a06ea27bb77f526db Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 2 Jan 2026 04:07:13 +0530 Subject: [PATCH 11/12] feat: integrate document upload dialog and enhance dashboard layout - Added DocumentUploadDialogProvider to manage document upload dialog state across components. - Updated DashboardClientLayout to include the DocumentUploadDialogProvider for improved user experience. - Refactored DocumentsTableShell to utilize the new dialog for file uploads instead of navigating to a separate upload page. - Removed the deprecated upload page and streamlined document upload handling within the dialog. - Enhanced DocumentUploadTab with improved file type handling and user feedback during uploads. - Updated GridPattern styling for better visual consistency. --- .../[search_space_id]/client-layout.tsx | 53 +-- .../components/DocumentsTableShell.tsx | 7 +- .../documents/upload/page.tsx | 36 -- .../components/assistant-ui/attachment.tsx | 16 +- .../assistant-ui/document-upload-popup.tsx | 101 +++++ .../components/sources/DocumentUploadTab.tsx | 370 ++++++++---------- .../components/sources/GridPattern.tsx | 6 +- 7 files changed, 316 insertions(+), 273 deletions(-) delete mode 100644 surfsense_web/app/dashboard/[search_space_id]/documents/upload/page.tsx create mode 100644 surfsense_web/components/assistant-ui/document-upload-popup.tsx diff --git a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx index 647c93282..1335c8bcb 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx @@ -20,6 +20,7 @@ import { AppSidebarProvider } from "@/components/sidebar/AppSidebarProvider"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; +import { DocumentUploadDialogProvider } from "@/components/assistant-ui/document-upload-popup"; export function DashboardClientLayout({ children, @@ -240,32 +241,34 @@ export function DashboardClientLayout({ } return ( - - {/* Use AppSidebarProvider which fetches user, search space, and recent chats */} - - -
-
-
-
- -
- - + + + {/* Use AppSidebarProvider which fetches user, search space, and recent chats */} + + +
+
+
+
+ +
+ + +
+
+
+
-
- -
-
-
-
{children}
-
-
-
+ +
{children}
+ + + + ); } 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 4800491f8..e933621cd 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 @@ -2,9 +2,10 @@ import { ChevronDown, ChevronUp, FileX, Plus } from "lucide-react"; import { motion } from "motion/react"; -import { useParams, useRouter } from "next/navigation"; +import { useParams } from "next/navigation"; import { useTranslations } from "next-intl"; import React from "react"; +import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup"; import { DocumentViewer } from "@/components/document-viewer"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -69,9 +70,9 @@ export function DocumentsTableShell({ onSortChange: (key: SortKey) => void; }) { const t = useTranslations("documents"); - const router = useRouter(); const params = useParams(); const searchSpaceId = params.search_space_id; + const { openDialog } = useDocumentUploadDialog(); const sorted = React.useMemo( () => sortDocuments(documents, sortKey, sortDesc), @@ -144,7 +145,7 @@ export function DocumentsTableShell({

- -
+ +
{files.map((file, index) => (
@@ -329,7 +295,7 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) { diff --git a/surfsense_web/components/assistant-ui/document-upload-popup.tsx b/surfsense_web/components/assistant-ui/document-upload-popup.tsx index c1a2512fb..f522b57dc 100644 --- a/surfsense_web/components/assistant-ui/document-upload-popup.tsx +++ b/surfsense_web/components/assistant-ui/document-upload-popup.tsx @@ -1,7 +1,15 @@ "use client"; import { useAtomValue } from "jotai"; -import { type FC, createContext, useContext, useState, useCallback, useRef, type ReactNode } from "react"; +import { + type FC, + createContext, + useContext, + useState, + useCallback, + useRef, + type ReactNode, +} from "react"; import { useRouter } from "next/navigation"; import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { Dialog, DialogContent } from "@/components/ui/dialog"; @@ -45,19 +53,22 @@ export const DocumentUploadDialogProvider: FC<{ children: ReactNode }> = ({ chil }, 300); }, []); - const handleOpenChange = useCallback((open: boolean) => { - if (!open) { - // Only close if not already in closing state - if (!isClosingRef.current) { - closeDialog(); + const handleOpenChange = useCallback( + (open: boolean) => { + if (!open) { + // Only close if not already in closing state + if (!isClosingRef.current) { + closeDialog(); + } + } else { + // Only open if not in the middle of closing + if (!isClosingRef.current) { + setIsOpen(true); + } } - } else { - // Only open if not in the middle of closing - if (!isClosingRef.current) { - setIsOpen(true); - } - } - }, [closeDialog]); + }, + [closeDialog] + ); return ( @@ -98,4 +109,3 @@ const DocumentUploadPopupContent: FC<{ ); }; - diff --git a/surfsense_web/components/sources/DocumentUploadTab.tsx b/surfsense_web/components/sources/DocumentUploadTab.tsx index 5676f21a9..e3328b9bd 100644 --- a/surfsense_web/components/sources/DocumentUploadTab.tsx +++ b/surfsense_web/components/sources/DocumentUploadTab.tsx @@ -9,7 +9,12 @@ import { useCallback, useMemo, useState, useRef } from "react"; import { useDropzone } from "react-dropzone"; import { toast } from "sonner"; import { uploadDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms"; -import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -201,8 +206,8 @@ export function DocumentUploadTab({ searchSpaceId, onSuccess }: DocumentUploadTa {...getRootProps()} className="flex flex-col items-center justify-center min-h-[200px] sm:min-h-[300px] border-2 border-dashed border-border rounded-lg hover:border-primary/50 transition-colors cursor-pointer" > - )}
-
-
@@ -352,14 +363,22 @@ export function DocumentUploadTab({ searchSpaceId, onSuccess }: DocumentUploadTa )}
- +
-
{t("supported_file_types")}
-
{t("file_types_desc")}
+
+ {t("supported_file_types")} +
+
+ {t("file_types_desc")} +