diff --git a/backend/onyx/server/documents/connector.py b/backend/onyx/server/documents/connector.py index 13010fd8e..e3d756e0b 100644 --- a/backend/onyx/server/documents/connector.py +++ b/backend/onyx/server/documents/connector.py @@ -92,6 +92,7 @@ from onyx.db.enums import IndexingMode from onyx.db.index_attempt import get_index_attempts_for_cc_pair from onyx.db.index_attempt import get_latest_index_attempts_by_status from onyx.db.index_attempt import get_latest_index_attempts_parallel +from onyx.db.models import Connector from onyx.db.models import ConnectorCredentialPair from onyx.db.models import IndexAttempt from onyx.db.models import IndexingStatus @@ -789,6 +790,7 @@ def get_connector_indexing_status( if latest_index_attempt else None ), + is_seeded=is_connector_seeded(connector), ) ) @@ -1243,9 +1245,18 @@ def get_connector_by_id( class BasicCCPairInfo(BaseModel): has_successful_run: bool + has_successful_sync_if_needs_sync: bool + seeded: bool source: DocumentSource +def is_connector_seeded(connector: Connector) -> bool: + return ( + connector.connector_specific_config.get("base_url") + == "https://docs.onyx.app/more/use_cases" + ) + + @router.get("/connector-status") def get_basic_connector_indexing_status( user: User = Depends(current_chat_accesssible_user), @@ -1257,10 +1268,16 @@ def get_basic_connector_indexing_status( get_editable=False, user=user, ) + return [ BasicCCPairInfo( has_successful_run=cc_pair.last_successful_index_time is not None, + has_successful_sync_if_needs_sync=( + cc_pair.last_time_perm_sync is not None + or cc_pair.access_type != AccessType.SYNC + ), source=cc_pair.connector.source, + seeded=is_connector_seeded(cc_pair.connector), ) for cc_pair in cc_pairs if cc_pair.connector.source != DocumentSource.INGESTION_API diff --git a/backend/onyx/server/documents/models.py b/backend/onyx/server/documents/models.py index ff1c4b2c8..48905810f 100644 --- a/backend/onyx/server/documents/models.py +++ b/backend/onyx/server/documents/models.py @@ -319,6 +319,7 @@ class ConnectorIndexingStatus(ConnectorStatus): latest_index_attempt: IndexAttemptSnapshot | None docs_indexed: int in_progress: bool + is_seeded: bool class ConnectorCredentialPairIdentifier(BaseModel): diff --git a/web/src/app/admin/connector/[ccPairId]/page.tsx b/web/src/app/admin/connector/[ccPairId]/page.tsx index 3f26179ce..d5e5236a3 100644 --- a/web/src/app/admin/connector/[ccPairId]/page.tsx +++ b/web/src/app/admin/connector/[ccPairId]/page.tsx @@ -421,7 +421,7 @@ function Main({ ccPairId }: { ccPairId: number }) {

{!ccPair.last_successful_index_time ? "This connector has never been successfully indexed. Documents from this connector will not appear in search results until indexing completes successfully." - : "Permissions synchronization is still in progress for this connector. Some documents may not appear in search results until this process completes."} + : "Permissions sync is still in progress for this connector. Some documents may not appear in search results until this process completes."}

diff --git a/web/src/app/admin/indexing/status/ConnectorCreatedSuccessModal.tsx b/web/src/app/admin/indexing/status/ConnectorCreatedSuccessModal.tsx new file mode 100644 index 000000000..b9a5d3141 --- /dev/null +++ b/web/src/app/admin/indexing/status/ConnectorCreatedSuccessModal.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { CheckmarkIcon } from "@/components/icons/icons"; + +export function ConnectorCreatedSuccessModal() { + const [open, setOpen] = useState(true); + const router = useRouter(); + + // Close the modal and update the URL to remove the query param + const handleClose = () => { + setOpen(false); + router.replace("/admin/indexing/status"); + }; + + return ( + + + +
+ +
+
+ + Congratulations! + + + You've successfully created your first connector. + +
+
+ +
+

+
+ Syncing in progress +

+

+ It will take some time to sync your documents. You'll know it's + complete when the "Last Indexed" field is filled in on the + Connectors page. +

+
+ + + + +
+
+ ); +} diff --git a/web/src/app/admin/indexing/status/page.tsx b/web/src/app/admin/indexing/status/page.tsx index e55fdc2a7..74f8dbc9a 100644 --- a/web/src/app/admin/indexing/status/page.tsx +++ b/web/src/app/admin/indexing/status/page.tsx @@ -7,10 +7,19 @@ import { AdminPageTitle } from "@/components/admin/Title"; import Link from "next/link"; import Text from "@/components/ui/text"; import { useConnectorCredentialIndexingStatus } from "@/lib/hooks"; -import { usePopupFromQuery } from "@/components/popup/PopupFromQuery"; +import { + PopupMessages, + usePopupFromQuery, +} from "@/components/popup/PopupFromQuery"; import { Button } from "@/components/ui/button"; +import { useSearchParams } from "next/navigation"; +import { ConnectorCreatedSuccessModal } from "./ConnectorCreatedSuccessModal"; +import { useMemo } from "react"; -function Main() { +// Constants +const ADD_CONNECTOR_PATH = "/admin/add-connector"; + +const ConnectorStatusList = () => { const { data: indexAttemptData, isLoading: indexAttemptIsLoading, @@ -23,10 +32,12 @@ function Main() { error: editableIndexAttemptError, } = useConnectorCredentialIndexingStatus(undefined, true); + // Handle loading state if (indexAttemptIsLoading || editableIndexAttemptIsLoading) { return ; } + // Handle error states if ( indexAttemptError || !indexAttemptData || @@ -42,11 +53,12 @@ function Main() { ); } + // Show empty state when no connectors if (indexAttemptData.length === 0) { return ( It looks like you don't have any connectors setup yet. Visit the{" "} - + Add Connector {" "} page to get started! @@ -54,36 +66,59 @@ function Main() { ); } - // sort by source name - indexAttemptData.sort((a, b) => { - if (a.connector.source < b.connector.source) { - return -1; - } else if (a.connector.source > b.connector.source) { - return 1; - } else { - return 0; - } - }); + // Sort data by source name + const sortedIndexAttemptData = [...indexAttemptData].sort((a, b) => + a.connector.source.localeCompare(b.connector.source) + ); return ( - + <> + + ); -} +}; export default function Status() { - const { popup } = usePopupFromQuery({ - "connector-created": { - message: "Connector created successfully", - type: "success", - }, + const searchParams = useSearchParams(); + const justCreatedConnector = + searchParams.get("message") === "connector-created"; + + // Use data to determine if we should show the popup or modal + const { data: indexAttemptData, isLoading: indexAttemptIsLoading } = + useConnectorCredentialIndexingStatus(); + + // Only show popup if we're not showing the success modal and there's exactly one seeded connector + const showSuccessModal = useMemo(() => { + return ( + !indexAttemptIsLoading && + indexAttemptData && + justCreatedConnector && + indexAttemptData.filter((attempt) => attempt.is_seeded).length === 1 + ); + }, [indexAttemptIsLoading, indexAttemptData]); + + // Create popup messages based on query parameters + const popupMessages: PopupMessages = { "connector-deleted": { message: "Connector deleted successfully", type: "success", }, - }); + }; + + // Conditionally add connector-created message + if (!showSuccessModal) { + Object.assign(popupMessages, { + "connector-created": { + message: "Connector created successfully", + type: "success", + }, + }); + } + + const { popup } = usePopupFromQuery(popupMessages); return (
@@ -92,13 +127,15 @@ export default function Status() { icon={} title="Existing Connectors" farRightElement={ - + } /> -
+ {showSuccessModal && } + +
); } diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index f2da48005..7ca575063 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -73,7 +73,7 @@ import { DocumentResults } from "./documentSidebar/DocumentResults"; import { OnyxInitializingLoader } from "@/components/OnyxInitializingLoader"; import { FeedbackModal } from "./modal/FeedbackModal"; import { ShareChatSessionModal } from "./modal/ShareChatSessionModal"; -import { FiArrowDown } from "react-icons/fi"; +import { FiArrowDown, FiExternalLink } from "react-icons/fi"; import { ChatIntro } from "./ChatIntro"; import { AIMessage, HumanMessage } from "./message/Messages"; import { StarterMessages } from "../../components/assistants/StarterMessage"; @@ -138,6 +138,10 @@ import { useSidebarShortcut } from "@/lib/browserUtilities"; import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal"; import { ChatSearchModal } from "./chat_search/ChatSearchModal"; import { ErrorBanner } from "./message/Resubmit"; +import { ExternalLinkIcon } from "lucide-react"; +import { XIcon } from "@/components/icons/icons"; +import { HIDE_NO_SOURCES_MESSAGE_COOKIE_NAME } from "@/lib/constants"; +import Link from "next/link"; const TEMP_USER_MESSAGE_ID = -1; const TEMP_ASSISTANT_MESSAGE_ID = -2; @@ -166,9 +170,19 @@ export function ChatPage({ folders, shouldShowWelcomeModal, refreshChatSessions, + showNoSourcesMessage: initialShowNoSourcesMessage, proSearchToggled, } = useChatContext(); + const [showNoSourcesMessage, setShowNoSourcesMessage] = useState( + initialShowNoSourcesMessage + ); + + const dismissNoSourcesMessage = () => { + Cookies.set(HIDE_NO_SOURCES_MESSAGE_COOKIE_NAME, "true"); + setShowNoSourcesMessage(false); + }; + const defaultAssistantIdRaw = searchParams.get(SEARCH_PARAM_NAMES.PERSONA_ID); const defaultAssistantId = defaultAssistantIdRaw ? parseInt(defaultAssistantIdRaw) @@ -201,6 +215,7 @@ export function ChatPage({ // available in server-side components const settings = useContext(SettingsContext); const enterpriseSettings = settings?.enterpriseSettings; + const [ready, setReady] = useState(false); const [documentSidebarVisible, setDocumentSidebarVisible] = useState(false); const [proSearchEnabled, setProSearchEnabled] = useState(proSearchToggled); @@ -3118,6 +3133,39 @@ export function ChatPage({ )} + {showNoSourcesMessage && ( +
+
+
+

+ No sources have been completed. Answers will + only include the seeded doc +

+ + + +
+
+ +
+
+ )} +
- {/* Right Sidebar - DocumentSidebar */} ); diff --git a/web/src/app/chat/layout.tsx b/web/src/app/chat/layout.tsx index 0ad9ce8a8..45ce3078f 100644 --- a/web/src/app/chat/layout.tsx +++ b/web/src/app/chat/layout.tsx @@ -37,6 +37,7 @@ export default async function Layout({ ccPairs, inputPrompts, proSearchToggled, + showNoSourcesMessage, } = data; return ( @@ -59,6 +60,7 @@ export default async function Layout({ openedFolders, shouldShowWelcomeModal, defaultAssistantId, + showNoSourcesMessage, }} > {children} diff --git a/web/src/app/ee/assistants/stats/[id]/page.tsx b/web/src/app/ee/assistants/stats/[id]/page.tsx index e667b5d3d..6f22be252 100644 --- a/web/src/app/ee/assistants/stats/[id]/page.tsx +++ b/web/src/app/ee/assistants/stats/[id]/page.tsx @@ -36,6 +36,7 @@ export default async function GalleryPage(props: { defaultAssistantId, inputPrompts, proSearchToggled, + showNoSourcesMessage, } = data; return ( @@ -56,6 +57,7 @@ export default async function GalleryPage(props: { openedFolders, shouldShowWelcomeModal, defaultAssistantId, + showNoSourcesMessage, }} > {shouldShowWelcomeModal && ( diff --git a/web/src/components/admin/Layout.tsx b/web/src/components/admin/Layout.tsx index a0bb88591..b3145b2a1 100644 --- a/web/src/components/admin/Layout.tsx +++ b/web/src/components/admin/Layout.tsx @@ -62,6 +62,7 @@ export async function Layout({ children }: { children: React.ReactNode }) { shouldShowWelcomeModal, ccPairs, inputPrompts, + showNoSourcesMessage, proSearchToggled, } = data; @@ -83,6 +84,7 @@ export async function Layout({ children }: { children: React.ReactNode }) { openedFolders, shouldShowWelcomeModal, defaultAssistantId, + showNoSourcesMessage, }} > Promise; inputPrompts: InputPrompt[]; proSearchToggled: boolean; + showNoSourcesMessage: boolean; } const ChatContext = createContext(undefined); diff --git a/web/src/components/popup/PopupFromQuery.tsx b/web/src/components/popup/PopupFromQuery.tsx index 6c3cde6fc..57440fe92 100644 --- a/web/src/components/popup/PopupFromQuery.tsx +++ b/web/src/components/popup/PopupFromQuery.tsx @@ -4,7 +4,7 @@ import { usePopup } from "../admin/connectors/Popup"; import { PopupSpec } from "../admin/connectors/Popup"; import { useRouter } from "next/navigation"; -interface PopupMessages { +export interface PopupMessages { [key: string]: PopupSpec; } diff --git a/web/src/lib/chat/fetchChatData.ts b/web/src/lib/chat/fetchChatData.ts index 91b98671f..90fd6d622 100644 --- a/web/src/lib/chat/fetchChatData.ts +++ b/web/src/lib/chat/fetchChatData.ts @@ -26,6 +26,7 @@ import { } from "@/components/resizable/constants"; import { hasCompletedWelcomeFlowSS } from "@/components/initialSetup/welcome/WelcomeModalWrapper"; import { + HIDE_NO_SOURCES_MESSAGE_COOKIE_NAME, NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN, NEXT_PUBLIC_ENABLE_CHROME_EXTENSION, } from "../constants"; @@ -47,6 +48,7 @@ interface FetchChatDataResult { shouldShowWelcomeModal: boolean; inputPrompts: InputPrompt[]; proSearchToggled: boolean; + showNoSourcesMessage: boolean; } export async function fetchChatData(searchParams: { @@ -130,6 +132,21 @@ export async function fetchChatData(searchParams: { } else { console.log(`Failed to fetch connectors - ${ccPairsResponse?.status}`); } + + const hideNoSourcesMessage = + requestCookies + .get(HIDE_NO_SOURCES_MESSAGE_COOKIE_NAME) + ?.value.toLowerCase() === "true"; + + const showNoSourcesMessage = !( + ccPairs.some( + (ccPair) => + !ccPair.seeded && + ccPair.has_successful_run && + ccPair.has_successful_sync_if_needs_sync + ) || hideNoSourcesMessage + ); + const availableSources: ValidSources[] = []; ccPairs.forEach((ccPair) => { if (!availableSources.includes(ccPair.source)) { @@ -234,5 +251,6 @@ export async function fetchChatData(searchParams: { shouldShowWelcomeModal, inputPrompts, proSearchToggled, + showNoSourcesMessage, }; } diff --git a/web/src/lib/connector.ts b/web/src/lib/connector.ts index 73eaafe61..d9fb04d8f 100644 --- a/web/src/lib/connector.ts +++ b/web/src/lib/connector.ts @@ -45,7 +45,9 @@ export async function updateConnectorCredentialPairName( newName: string ): Promise { return fetch( - `/api/manage/admin/cc-pair/${ccPairId}/name?new_name=${encodeURIComponent(newName)}`, + `/api/manage/admin/cc-pair/${ccPairId}/name?new_name=${encodeURIComponent( + newName + )}`, { method: "PUT", headers: { diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 37fff721d..1c787378d 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -94,3 +94,5 @@ export const NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK = export const NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY; + +export const HIDE_NO_SOURCES_MESSAGE_COOKIE_NAME = "hide_no_sources_message"; diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index b6b9d4c88..6dbbeb67e 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -162,6 +162,7 @@ export interface ConnectorIndexingStatus< cc_pair_status: ConnectorCredentialPairStatus; latest_index_attempt: IndexAttemptSnapshot | null; docs_indexed: number; + is_seeded: boolean; } export interface OAuthPrepareAuthorizationResponse { @@ -203,6 +204,8 @@ export interface OAuthConfluenceFinalizeResponse { export interface CCPairBasicInfo { has_successful_run: boolean; source: ValidSources; + seeded: boolean; + has_successful_sync_if_needs_sync: boolean; } export type ConnectorSummary = {