From af647959f6dbaa5c410d442f84c0c3942fd55b8b Mon Sep 17 00:00:00 2001 From: Chris Weaver <25087905+Weves@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:07:00 -0700 Subject: [PATCH] Performance Improvements (#2162) --- ...9164_chosen_assistants_changed_to_jsonb.py | 65 ++++ backend/danswer/auth/users.py | 4 +- backend/danswer/configs/app_configs.py | 3 + backend/danswer/db/chat.py | 4 + backend/danswer/db/engine.py | 34 ++- backend/danswer/db/index_attempt.py | 24 +- backend/danswer/db/models.py | 2 +- backend/danswer/db/persona.py | 13 +- backend/danswer/server/documents/connector.py | 1 - .../danswer/server/features/persona/api.py | 6 +- .../docker_compose/docker-compose.dev.yml | 4 +- web/src/app/assistants/gallery/page.tsx | 43 +-- .../assistants/mine/WrappedInputPrompts.tsx | 1 - web/src/app/assistants/mine/page.tsx | 41 +-- web/src/app/chat/ChatPage.tsx | 6 +- .../chat/modal/configuration/FiltersTab.tsx | 281 ------------------ web/src/app/chat/page.tsx | 3 - .../chat/sessionSidebar/HistorySidebar.tsx | 10 +- web/src/app/layout.tsx | 28 +- web/src/app/search/page.tsx | 2 +- web/src/components/settings/lib.ts | 18 +- web/src/lib/chat/fetchSomeChatData.ts | 236 +++++++++++++++ 22 files changed, 426 insertions(+), 403 deletions(-) create mode 100644 backend/alembic/versions/da4c21c69164_chosen_assistants_changed_to_jsonb.py delete mode 100644 web/src/app/chat/modal/configuration/FiltersTab.tsx create mode 100644 web/src/lib/chat/fetchSomeChatData.ts diff --git a/backend/alembic/versions/da4c21c69164_chosen_assistants_changed_to_jsonb.py b/backend/alembic/versions/da4c21c69164_chosen_assistants_changed_to_jsonb.py new file mode 100644 index 000000000000..e94ab75fb178 --- /dev/null +++ b/backend/alembic/versions/da4c21c69164_chosen_assistants_changed_to_jsonb.py @@ -0,0 +1,65 @@ +"""chosen_assistants changed to jsonb + +Revision ID: da4c21c69164 +Revises: c5b692fa265c +Create Date: 2024-08-18 19:06:47.291491 + +""" +import json +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "da4c21c69164" +down_revision = "c5b692fa265c" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + conn = op.get_bind() + existing_ids_and_chosen_assistants = conn.execute( + sa.text("select id, chosen_assistants from public.user") + ) + op.drop_column( + "user", + "chosen_assistants", + ) + op.add_column( + "user", + sa.Column( + "chosen_assistants", + postgresql.JSONB(astext_type=sa.Text()), + nullable=True, + ), + ) + for id, chosen_assistants in existing_ids_and_chosen_assistants: + conn.execute( + sa.text( + "update public.user set chosen_assistants = :chosen_assistants where id = :id" + ), + {"chosen_assistants": json.dumps(chosen_assistants), "id": id}, + ) + + +def downgrade() -> None: + conn = op.get_bind() + existing_ids_and_chosen_assistants = conn.execute( + sa.text("select id, chosen_assistants from public.user") + ) + op.drop_column( + "user", + "chosen_assistants", + ) + op.add_column( + "user", + sa.Column("chosen_assistants", postgresql.ARRAY(sa.Integer()), nullable=True), + ) + for id, chosen_assistants in existing_ids_and_chosen_assistants: + conn.execute( + sa.text( + "update public.user set chosen_assistants = :chosen_assistants where id = :id" + ), + {"chosen_assistants": chosen_assistants, "id": id}, + ) diff --git a/backend/danswer/auth/users.py b/backend/danswer/auth/users.py index ce3b8e88f359..76b4ca812c95 100644 --- a/backend/danswer/auth/users.py +++ b/backend/danswer/auth/users.py @@ -59,9 +59,7 @@ from danswer.db.users import get_user_by_email from danswer.utils.logger import setup_logger from danswer.utils.telemetry import optional_telemetry from danswer.utils.telemetry import RecordType -from danswer.utils.variable_functionality import ( - fetch_versioned_implementation, -) +from danswer.utils.variable_functionality import fetch_versioned_implementation logger = setup_logger() diff --git a/backend/danswer/configs/app_configs.py b/backend/danswer/configs/app_configs.py index d89c39ca8cce..cccb81c9aa22 100644 --- a/backend/danswer/configs/app_configs.py +++ b/backend/danswer/configs/app_configs.py @@ -326,6 +326,9 @@ LOG_VESPA_TIMING_INFORMATION = ( ) LOG_ENDPOINT_LATENCY = os.environ.get("LOG_ENDPOINT_LATENCY", "").lower() == "true" LOG_POSTGRES_LATENCY = os.environ.get("LOG_POSTGRES_LATENCY", "").lower() == "true" +LOG_POSTGRES_CONN_COUNTS = ( + os.environ.get("LOG_POSTGRES_CONN_COUNTS", "").lower() == "true" +) # Anonymous usage telemetry DISABLE_TELEMETRY = os.environ.get("DISABLE_TELEMETRY", "").lower() == "true" diff --git a/backend/danswer/db/chat.py b/backend/danswer/db/chat.py index 301c481033d5..06ece1e922f3 100644 --- a/backend/danswer/db/chat.py +++ b/backend/danswer/db/chat.py @@ -117,6 +117,7 @@ def get_chat_sessions_by_user( deleted: bool | None, db_session: Session, only_one_shot: bool = False, + limit: int = 50, ) -> list[ChatSession]: stmt = select(ChatSession).where(ChatSession.user_id == user_id) @@ -130,6 +131,9 @@ def get_chat_sessions_by_user( if deleted is not None: stmt = stmt.where(ChatSession.deleted == deleted) + if limit: + stmt = stmt.limit(limit) + result = db_session.execute(stmt) chat_sessions = result.scalars().all() diff --git a/backend/danswer/db/engine.py b/backend/danswer/db/engine.py index 6268018901cc..94b5d0123ccc 100644 --- a/backend/danswer/db/engine.py +++ b/backend/danswer/db/engine.py @@ -15,6 +15,7 @@ from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy.orm import Session from sqlalchemy.orm import sessionmaker +from danswer.configs.app_configs import LOG_POSTGRES_CONN_COUNTS from danswer.configs.app_configs import LOG_POSTGRES_LATENCY from danswer.configs.app_configs import POSTGRES_DB from danswer.configs.app_configs import POSTGRES_HOST @@ -65,6 +66,37 @@ if LOG_POSTGRES_LATENCY: ) +if LOG_POSTGRES_CONN_COUNTS: + # Global counter for connection checkouts and checkins + checkout_count = 0 + checkin_count = 0 + + @event.listens_for(Engine, "checkout") + def log_checkout(dbapi_connection, connection_record, connection_proxy): # type: ignore + global checkout_count + checkout_count += 1 + + active_connections = connection_proxy._pool.checkedout() + idle_connections = connection_proxy._pool.checkedin() + pool_size = connection_proxy._pool.size() + logger.debug( + "Connection Checkout\n" + f"Active Connections: {active_connections};\n" + f"Idle: {idle_connections};\n" + f"Pool Size: {pool_size};\n" + f"Total connection checkouts: {checkout_count}" + ) + + @event.listens_for(Engine, "checkin") + def log_checkin(dbapi_connection, connection_record): # type: ignore + global checkin_count + checkin_count += 1 + logger.debug(f"Total connection checkins: {checkin_count}") + + +"""END DEBUGGING LOGGING""" + + def get_db_current_time(db_session: Session) -> datetime: """Get the current time from Postgres representing the start of the transaction Within the same transaction this value will not update @@ -152,7 +184,7 @@ async def get_async_session() -> AsyncGenerator[AsyncSession, None]: async def warm_up_connections( - sync_connections_to_warm_up: int = 10, async_connections_to_warm_up: int = 10 + sync_connections_to_warm_up: int = 20, async_connections_to_warm_up: int = 20 ) -> None: sync_postgres_engine = get_sqlalchemy_engine() connections = [ diff --git a/backend/danswer/db/index_attempt.py b/backend/danswer/db/index_attempt.py index a87f8f45f1ab..3d8668427b41 100644 --- a/backend/danswer/db/index_attempt.py +++ b/backend/danswer/db/index_attempt.py @@ -1,11 +1,9 @@ from collections.abc import Sequence from sqlalchemy import and_ -from sqlalchemy import ColumnElement from sqlalchemy import delete from sqlalchemy import desc from sqlalchemy import func -from sqlalchemy import or_ from sqlalchemy import select from sqlalchemy import update from sqlalchemy.orm import joinedload @@ -184,13 +182,12 @@ def get_last_attempt( def get_latest_index_attempts( - connector_credential_pair_identifiers: list[ConnectorCredentialPairIdentifier], secondary_index: bool, db_session: Session, ) -> Sequence[IndexAttempt]: ids_stmt = select( IndexAttempt.connector_credential_pair_id, - func.max(IndexAttempt.time_created).label("max_time_created"), + func.max(IndexAttempt.id).label("max_id"), ).join(EmbeddingModel, IndexAttempt.embedding_model_id == EmbeddingModel.id) if secondary_index: @@ -198,23 +195,6 @@ def get_latest_index_attempts( else: ids_stmt = ids_stmt.where(EmbeddingModel.status == IndexModelStatus.PRESENT) - where_stmts: list[ColumnElement] = [] - for connector_credential_pair_identifier in connector_credential_pair_identifiers: - where_stmts.append( - IndexAttempt.connector_credential_pair_id - == ( - select(ConnectorCredentialPair.id) - .where( - ConnectorCredentialPair.connector_id - == connector_credential_pair_identifier.connector_id, - ConnectorCredentialPair.credential_id - == connector_credential_pair_identifier.credential_id, - ) - .scalar_subquery() - ) - ) - if where_stmts: - ids_stmt = ids_stmt.where(or_(*where_stmts)) ids_stmt = ids_stmt.group_by(IndexAttempt.connector_credential_pair_id) ids_subquery = ids_stmt.subquery() @@ -225,7 +205,7 @@ def get_latest_index_attempts( IndexAttempt.connector_credential_pair_id == ids_subquery.c.connector_credential_pair_id, ) - .where(IndexAttempt.time_created == ids_subquery.c.max_time_created) + .where(IndexAttempt.id == ids_subquery.c.max_id) ) return db_session.execute(stmt).scalars().all() diff --git a/backend/danswer/db/models.py b/backend/danswer/db/models.py index cd8f1721c3b3..c92b6c3c153b 100644 --- a/backend/danswer/db/models.py +++ b/backend/danswer/db/models.py @@ -120,7 +120,7 @@ class User(SQLAlchemyBaseUserTableUUID, Base): # if specified, controls the assistants that are shown to the user + their order # if not specified, all assistants are shown chosen_assistants: Mapped[list[int]] = mapped_column( - postgresql.ARRAY(Integer), nullable=True + postgresql.JSONB(), nullable=True ) oidc_expiry: Mapped[datetime.datetime] = mapped_column( diff --git a/backend/danswer/db/persona.py b/backend/danswer/db/persona.py index 067ae477ae68..8c25b27b961f 100644 --- a/backend/danswer/db/persona.py +++ b/backend/danswer/db/persona.py @@ -9,6 +9,7 @@ from sqlalchemy import not_ from sqlalchemy import or_ from sqlalchemy import select from sqlalchemy import update +from sqlalchemy.orm import joinedload from sqlalchemy.orm import selectinload from sqlalchemy.orm import Session @@ -169,6 +170,7 @@ def get_personas( include_default: bool = True, include_slack_bot_personas: bool = False, include_deleted: bool = False, + joinedload_all: bool = False, ) -> Sequence[Persona]: stmt = select(Persona).distinct() if user_id is not None: @@ -200,7 +202,16 @@ def get_personas( if not include_deleted: stmt = stmt.where(Persona.deleted.is_(False)) - return db_session.scalars(stmt).all() + if joinedload_all: + stmt = stmt.options( + joinedload(Persona.prompts), + joinedload(Persona.tools), + joinedload(Persona.document_sets), + joinedload(Persona.groups), + joinedload(Persona.users), + ) + + return db_session.execute(stmt).unique().scalars().all() def mark_persona_as_deleted( diff --git a/backend/danswer/server/documents/connector.py b/backend/danswer/server/documents/connector.py index e78b2ebb85ca..abc9de1f9dd6 100644 --- a/backend/danswer/server/documents/connector.py +++ b/backend/danswer/server/documents/connector.py @@ -387,7 +387,6 @@ def get_connector_indexing_status( ] latest_index_attempts = get_latest_index_attempts( - connector_credential_pair_identifiers=cc_pair_identifiers, secondary_index=secondary_index, db_session=db_session, ) diff --git a/backend/danswer/server/features/persona/api.py b/backend/danswer/server/features/persona/api.py index cf2c0e26174d..2ea68f5812c4 100644 --- a/backend/danswer/server/features/persona/api.py +++ b/backend/danswer/server/features/persona/api.py @@ -79,6 +79,7 @@ def list_personas_admin( db_session=db_session, user_id=None, # user_id = None -> give back all personas include_deleted=include_deleted, + joinedload_all=True, ) ] @@ -190,7 +191,10 @@ def list_personas( return [ PersonaSnapshot.from_model(persona) for persona in get_personas( - user_id=user_id, include_deleted=include_deleted, db_session=db_session + user_id=user_id, + include_deleted=include_deleted, + db_session=db_session, + joinedload_all=True, ) ] diff --git a/deployment/docker_compose/docker-compose.dev.yml b/deployment/docker_compose/docker-compose.dev.yml index 0bbe15b8d180..ea5e8e1e5d5b 100644 --- a/deployment/docker_compose/docker-compose.dev.yml +++ b/deployment/docker_compose/docker-compose.dev.yml @@ -88,7 +88,9 @@ services: # (time spent on finding the right docs + time spent fetching summaries from disk) - LOG_VESPA_TIMING_INFORMATION=${LOG_VESPA_TIMING_INFORMATION:-} - LOG_ENDPOINT_LATENCY=${LOG_ENDPOINT_LATENCY:-} - + - LOG_POSTGRES_LATENCY=${LOG_POSTGRES_LATENCY:-} + - LOG_POSTGRES_CONN_COUNTS=${LOG_POSTGRES_CONN_COUNTS:-} + # Enterprise Edition only - ENABLE_PAID_ENTERPRISE_EDITION_FEATURES=${ENABLE_PAID_ENTERPRISE_EDITION_FEATURES:-false} - API_KEY_HASH_ROUNDS=${API_KEY_HASH_ROUNDS:-} diff --git a/web/src/app/assistants/gallery/page.tsx b/web/src/app/assistants/gallery/page.tsx index 6ac8d61c7328..e955eb0117e4 100644 --- a/web/src/app/assistants/gallery/page.tsx +++ b/web/src/app/assistants/gallery/page.tsx @@ -1,7 +1,4 @@ -import { HistorySidebar } from "@/app/chat/sessionSidebar/HistorySidebar"; import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh"; -import { UserDropdown } from "@/components/UserDropdown"; -import { ChatProvider } from "@/components/context/ChatContext"; import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper"; import { fetchChatData } from "@/lib/chat/fetchChatData"; import { unstable_noStore as noStore } from "next/cache"; @@ -24,47 +21,27 @@ export default async function GalleryPage({ const { user, chatSessions, - availableSources, - documentSets, assistants, - tags, - llmProviders, folders, openedFolders, shouldShowWelcomeModal, toggleSidebar, - userInputPrompts, } = data; return ( <> - - {shouldShowWelcomeModal && } - - - + + + ); } diff --git a/web/src/app/assistants/mine/WrappedInputPrompts.tsx b/web/src/app/assistants/mine/WrappedInputPrompts.tsx index cc7fb37dda77..4428b5244c31 100644 --- a/web/src/app/assistants/mine/WrappedInputPrompts.tsx +++ b/web/src/app/assistants/mine/WrappedInputPrompts.tsx @@ -48,7 +48,6 @@ export default function WrappedPrompts({ content={(contentProps) => (
Prompt Gallery - - - {shouldShowWelcomeModal && } - - - + + + ); } diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index a28c189120a9..52f92593463a 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -1,7 +1,6 @@ "use client"; -import ReactMarkdown from "react-markdown"; -import { redirect, useRouter, useSearchParams } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { BackendChatSession, BackendMessage, @@ -23,7 +22,6 @@ import Cookies from "js-cookie"; import { HistorySidebar } from "./sessionSidebar/HistorySidebar"; import { Persona } from "../admin/assistants/interfaces"; import { HealthCheckBanner } from "@/components/health/healthcheck"; -import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh"; import { buildChatUrl, buildLatestMessageChain, @@ -84,7 +82,6 @@ import FixedLogo from "./shared_chat_search/FixedLogo"; import { getSecondsUntilExpiration } from "@/lib/time"; import { SetDefaultModelModal } from "./modal/SetDefaultModelModal"; import { DeleteChatModal } from "./modal/DeleteChatModal"; -import remarkGfm from "remark-gfm"; import { MinimalMarkdown } from "@/components/chat_search/MinimalMarkdown"; import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal"; @@ -1304,7 +1301,6 @@ export function ChatPage({ return ( <> - {/* ChatPopup is a custom popup that displays a admin-specified message on initial user visit. Only used in the EE version of the app. */} {popup} diff --git a/web/src/app/chat/modal/configuration/FiltersTab.tsx b/web/src/app/chat/modal/configuration/FiltersTab.tsx deleted file mode 100644 index 66e559ae5832..000000000000 --- a/web/src/app/chat/modal/configuration/FiltersTab.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import { useChatContext } from "@/components/context/ChatContext"; -import { FilterManager } from "@/lib/hooks"; -import { listSourceMetadata } from "@/lib/sources"; -import { useEffect, useRef, useState } from "react"; -import { - DateRangePicker, - DateRangePickerItem, - Divider, - Text, -} from "@tremor/react"; -import { getXDaysAgo } from "@/lib/dateUtils"; -import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelectable"; -import { Bubble } from "@/components/Bubble"; -import { FiX } from "react-icons/fi"; -import { getValidTags } from "@/lib/tags/tagUtils"; -import debounce from "lodash/debounce"; -import { Tag } from "@/lib/types"; - -export function FiltersTab({ - filterManager, -}: { - filterManager: FilterManager; -}): JSX.Element { - const { availableSources, availableDocumentSets, availableTags } = - useChatContext(); - - const [filterValue, setFilterValue] = useState(""); - const [filteredTags, setFilteredTags] = useState(availableTags); - const inputRef = useRef(null); - - const allSources = listSourceMetadata(); - const availableSourceMetadata = allSources.filter((source) => - availableSources.includes(source.internalName) - ); - - const debouncedFetchTags = useRef( - debounce(async (value: string) => { - if (value) { - const fetchedTags = await getValidTags(value); - setFilteredTags(fetchedTags); - } else { - setFilteredTags(availableTags); - } - }, 50) - ).current; - - useEffect(() => { - debouncedFetchTags(filterValue); - - return () => { - debouncedFetchTags.cancel(); - }; - }, [filterValue, availableTags, debouncedFetchTags]); - - return ( -
-
-
-
-

Time Range

- - Choose the time range we should search over. If only one date is - selected, will only search after the specified date. - -
- - filterManager.setTimeRange({ - from: value.from, - to: value.to, - selectValue: value.selectValue, - }) - } - selectPlaceholder="Select range" - enableSelect - > - - Last 30 Days - - - Last 7 Days - - - Today - - -
-
- - - -
-

Knowledge Sets

- - Choose which knowledge sets we should search over. If multiple are - selected, we will search through all of them. - -
    - {availableDocumentSets.length > 0 ? ( - availableDocumentSets.map((set) => { - const isSelected = - filterManager.selectedDocumentSets.includes(set.name); - return ( - - filterManager.setSelectedDocumentSets((prev) => - isSelected - ? prev.filter((s) => s !== set.name) - : [...prev, set.name] - ) - } - /> - ); - }) - ) : ( -
  • No knowledge sets available
  • - )} -
-
- - - -
-

Sources

- - Choose which sources we should search over. If multiple sources - are selected, we will search through all of them. - -
    - {availableSourceMetadata.length > 0 ? ( - availableSourceMetadata.map((sourceMetadata) => { - const isSelected = filterManager.selectedSources.some( - (selectedSource) => - selectedSource.internalName === - sourceMetadata.internalName - ); - return ( - - filterManager.setSelectedSources((prev) => - isSelected - ? prev.filter( - (s) => - s.internalName !== sourceMetadata.internalName - ) - : [...prev, sourceMetadata] - ) - } - showCheckbox={true} - > -
    - {sourceMetadata?.icon({ size: 16 })} - {sourceMetadata.displayName} -
    -
    - ); - }) - ) : ( -
  • No sources available
  • - )} -
-
- - - -
-

Tags

-
    - {filterManager.selectedTags.length > 0 ? ( - filterManager.selectedTags.map((tag) => ( - - filterManager.setSelectedTags((prev) => - prev.filter( - (t) => - t.tag_key !== tag.tag_key || - t.tag_value !== tag.tag_value - ) - ) - } - > -
    -

    - {tag.tag_key}={tag.tag_value} -

    {" "} - -
    -
    - )) - ) : ( -

    No selected tags

    - )} -
- -
-
-
- setFilterValue(event.target.value)} - /> -
- -
- {filteredTags.length > 0 ? ( - filteredTags - .filter( - (tag) => - !filterManager.selectedTags.some( - (selectedTag) => - selectedTag.tag_key === tag.tag_key && - selectedTag.tag_value === tag.tag_value - ) - ) - .slice(0, 12) - .map((tag) => ( - - filterManager.setSelectedTags((prev) => - filterManager.selectedTags.includes(tag) - ? prev.filter( - (t) => - t.tag_key !== tag.tag_key || - t.tag_value !== tag.tag_value - ) - : [...prev, tag] - ) - } - > - <> - {tag.tag_key}={tag.tag_value} - - - )) - ) : ( -
- No matching tags found -
- )} -
-
-
-
-
-
-
- ); -} diff --git a/web/src/app/chat/page.tsx b/web/src/app/chat/page.tsx index 48c52dbf2e25..f5dce4617d1e 100644 --- a/web/src/app/chat/page.tsx +++ b/web/src/app/chat/page.tsx @@ -3,11 +3,8 @@ import { unstable_noStore as noStore } from "next/cache"; import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh"; import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper"; import { ApiKeyModal } from "@/components/llm/ApiKeyModal"; -import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal"; import { ChatProvider } from "@/components/context/ChatContext"; import { fetchChatData } from "@/lib/chat/fetchChatData"; -import FunctionalWrapper from "./shared_chat_search/FunctionalWrapper"; -import { ChatPage } from "./ChatPage"; import WrappedChat from "./WrappedChat"; export default async function Page({ diff --git a/web/src/app/chat/sessionSidebar/HistorySidebar.tsx b/web/src/app/chat/sessionSidebar/HistorySidebar.tsx index 8e08aaf379af..a7189b434a4b 100644 --- a/web/src/app/chat/sessionSidebar/HistorySidebar.tsx +++ b/web/src/app/chat/sessionSidebar/HistorySidebar.tsx @@ -69,11 +69,11 @@ export const HistorySidebar = forwardRef( const currentChatId = currentChatSession?.id; - // prevent the NextJS Router cache from causing the chat sidebar to not - // update / show an outdated list of chats - useEffect(() => { - router.refresh(); - }, [currentChatId]); + // NOTE: do not do something like the below - assume that the parent + // will handle properly refreshing the existingChats + // useEffect(() => { + // router.refresh(); + // }, [currentChatId]); const combinedSettings = useContext(SettingsContext); if (!combinedSettings) { diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 4328be873bd6..3e366555e19f 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -1,12 +1,19 @@ import "./globals.css"; -import { getCombinedSettings } from "@/components/settings/lib"; -import { CUSTOM_ANALYTICS_ENABLED } from "@/lib/constants"; +import { + fetchEnterpriseSettingsSS, + getCombinedSettings, +} from "@/components/settings/lib"; +import { + CUSTOM_ANALYTICS_ENABLED, + SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED, +} from "@/lib/constants"; import { SettingsProvider } from "@/components/settings/SettingsProvider"; import { Metadata } from "next"; import { buildClientUrl } from "@/lib/utilsSS"; import { Inter } from "next/font/google"; import Head from "next/head"; +import { EnterpriseSettings } from "./admin/settings/interfaces"; const inter = Inter({ subsets: ["latin"], @@ -15,15 +22,18 @@ const inter = Inter({ }); export async function generateMetadata(): Promise { - const dynamicSettings = await getCombinedSettings({ forceRetrieval: true }); - const logoLocation = - dynamicSettings.enterpriseSettings && - dynamicSettings.enterpriseSettings?.use_custom_logo - ? "/api/enterprise-settings/logo" - : buildClientUrl("/danswer.ico"); + let logoLocation = buildClientUrl("/danswer.ico"); + let enterpriseSettings: EnterpriseSettings | null = null; + if (SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED) { + enterpriseSettings = await (await fetchEnterpriseSettingsSS()).json(); + logoLocation = + enterpriseSettings && enterpriseSettings.use_custom_logo + ? "/api/enterprise-settings/logo" + : buildClientUrl("/danswer.ico"); + } return { - title: dynamicSettings.enterpriseSettings?.application_name ?? "Danswer", + title: enterpriseSettings?.application_name ?? "Danswer", description: "Question answering for your documents", icons: { icon: logoLocation, diff --git a/web/src/app/search/page.tsx b/web/src/app/search/page.tsx index 2cc53e474654..d1a7d0daae3c 100644 --- a/web/src/app/search/page.tsx +++ b/web/src/app/search/page.tsx @@ -185,6 +185,7 @@ export default async function Home() { <> {shouldShowWelcomeModal && } + {!shouldShowWelcomeModal && !shouldDisplayNoSourcesModal && @@ -200,7 +201,6 @@ export default async function Home() { Only used in the EE version of the app. */} - ; + defaultAssistantId?: number; + toggleSidebar?: boolean; + finalDocumentSidebarInitialWidth?: number; + shouldShowWelcomeModal?: boolean; + shouldDisplaySourcesIncompleteModal?: boolean; + userInputPrompts?: InputPrompt[]; +} + +type FetchOption = + | "user" + | "chatSessions" + | "ccPairs" + | "documentSets" + | "assistants" + | "tags" + | "llmProviders" + | "folders" + | "userInputPrompts"; + +/* +NOTE: currently unused, but leaving here for future use. +*/ +export async function fetchSomeChatData( + searchParams: { [key: string]: string }, + fetchOptions: FetchOption[] = [] +): Promise { + const tasks: Promise[] = []; + const taskMap: Record Promise> = { + user: getCurrentUserSS, + chatSessions: () => fetchSS("/chat/get-user-chat-sessions"), + ccPairs: () => fetchSS("/manage/indexing-status"), + documentSets: () => fetchSS("/manage/document-set"), + assistants: fetchAssistantsSS, + tags: () => fetchSS("/query/valid-tags"), + llmProviders: fetchLLMProvidersSS, + folders: () => fetchSS("/folder"), + userInputPrompts: () => fetchSS("/input_prompt?include_public=true"), + }; + + // Always fetch auth type metadata + tasks.push(getAuthTypeMetadataSS()); + + // Add tasks based on fetchOptions + fetchOptions.forEach((option) => { + if (taskMap[option]) { + tasks.push(taskMap[option]()); + } + }); + + let results: any[] = await Promise.all(tasks); + + const authTypeMetadata = results.shift() as AuthTypeMetadata | null; + const authDisabled = authTypeMetadata?.authType === "disabled"; + + let user: User | null = null; + if (fetchOptions.includes("user")) { + user = results.shift(); + if (!authDisabled && !user) { + return { redirect: "/auth/login" }; + } + if (user && !user.is_verified && authTypeMetadata?.requiresVerification) { + return { redirect: "/auth/waiting-on-verification" }; + } + } + + const result: FetchChatDataResult = {}; + + for (let i = 0; i < fetchOptions.length; i++) { + const option = fetchOptions[i]; + const result = results[i]; + + switch (option) { + case "user": + result.user = user; + break; + case "chatSessions": + result.chatSessions = result?.ok + ? ((await result.json()) as { sessions: ChatSession[] }).sessions + : []; + break; + case "ccPairs": + result.ccPairs = result?.ok + ? ((await result.json()) as CCPairBasicInfo[]) + : []; + break; + case "documentSets": + result.documentSets = result?.ok + ? ((await result.json()) as DocumentSet[]) + : []; + break; + case "assistants": + const [rawAssistantsList, assistantsFetchError] = result as [ + Persona[], + string | null, + ]; + result.assistants = rawAssistantsList + .filter((assistant) => assistant.is_visible) + .sort(personaComparator); + break; + case "tags": + result.tags = result?.ok + ? ((await result.json()) as { tags: Tag[] }).tags + : []; + break; + case "llmProviders": + result.llmProviders = result || []; + break; + case "folders": + result.folders = result?.ok + ? ((await result.json()) as { folders: Folder[] }).folders + : []; + break; + case "userInputPrompts": + result.userInputPrompts = result?.ok + ? ((await result.json()) as InputPrompt[]) + : []; + break; + } + } + + if (result.ccPairs) { + result.availableSources = Array.from( + new Set(result.ccPairs.map((ccPair) => ccPair.source)) + ); + } + + if (result.chatSessions) { + result.chatSessions.sort((a, b) => (a.id > b.id ? -1 : 1)); + } + + if (fetchOptions.includes("assistants") && result.assistants) { + const hasAnyConnectors = result.ccPairs && result.ccPairs.length > 0; + if (!hasAnyConnectors) { + result.assistants = result.assistants.filter( + (assistant) => assistant.num_chunks === 0 + ); + } + + const hasOpenAIProvider = + result.llmProviders && + result.llmProviders.some((provider) => provider.provider === "openai"); + if (!hasOpenAIProvider) { + result.assistants = result.assistants.filter( + (assistant) => + !assistant.tools.some( + (tool) => tool.in_code_tool_id === "ImageGenerationTool" + ) + ); + } + } + + if (fetchOptions.includes("folders")) { + const openedFoldersCookie = cookies().get("openedFolders"); + result.openedFolders = openedFoldersCookie + ? JSON.parse(openedFoldersCookie.value) + : {}; + } + + const defaultAssistantIdRaw = searchParams["assistantId"]; + result.defaultAssistantId = defaultAssistantIdRaw + ? parseInt(defaultAssistantIdRaw) + : undefined; + + const documentSidebarCookieInitialWidth = cookies().get( + DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME + ); + const sidebarToggled = cookies().get(SIDEBAR_TOGGLED_COOKIE_NAME); + + result.toggleSidebar = sidebarToggled + ? sidebarToggled.value.toLowerCase() === "true" + : NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN; + + result.finalDocumentSidebarInitialWidth = documentSidebarCookieInitialWidth + ? parseInt(documentSidebarCookieInitialWidth.value) + : undefined; + + if (fetchOptions.includes("ccPairs") && result.ccPairs) { + const hasAnyConnectors = result.ccPairs.length > 0; + result.shouldShowWelcomeModal = + !hasCompletedWelcomeFlowSS() && + !hasAnyConnectors && + (!user || user.role === "admin"); + + result.shouldDisplaySourcesIncompleteModal = + hasAnyConnectors && + !result.shouldShowWelcomeModal && + !result.ccPairs.some( + (ccPair) => ccPair.has_successful_run && ccPair.docs_indexed > 0 + ) && + (!user || user.role === "admin"); + } + + return result; +}