From c4e1c62c00584be049f99d2ca457a0b17f616f4f Mon Sep 17 00:00:00 2001 From: pablodanswer Date: Wed, 7 Aug 2024 14:55:16 -0700 Subject: [PATCH] Admin UX updates (#2057) --- .../one_shot_answer/answer_question.py | 2 - backend/danswer/search/pipeline.py | 9 +- backend/danswer/server/documents/connector.py | 5 +- backend/danswer/server/documents/models.py | 4 + backend/danswer/tools/search/search_tool.py | 2 +- web/src/app/admin/add-connector/page.tsx | 9 +- .../connector/[ccPairId]/ConfigDisplay.tsx | 56 +++++ .../app/admin/connector/[ccPairId]/page.tsx | 17 +- .../[connector]/AddConnectorPage.tsx | 4 +- .../modal/configuration/AssistantsTab.tsx | 2 +- web/src/app/globals.css | 22 ++ .../admin/connectors/ConnectorForm.tsx | 7 +- .../components/assistants/AssistantCards.tsx | 19 +- web/src/components/icons/icons.tsx | 69 ++++++ web/src/components/search/DocumentDisplay.tsx | 54 +++-- .../search/DocumentFeedbackBlock.tsx | 34 +-- web/src/components/search/QAFeedback.tsx | 30 +-- .../search/SearchResultsDisplay.tsx | 4 +- web/src/components/search/SearchSection.tsx | 221 ++++++++++++++---- .../search/results/QuotesSection.tsx | 4 +- .../search/results/ResponseSection.tsx | 12 +- web/src/lib/search/streamingQa.ts | 13 +- 22 files changed, 467 insertions(+), 132 deletions(-) diff --git a/backend/danswer/one_shot_answer/answer_question.py b/backend/danswer/one_shot_answer/answer_question.py index 2c090d248..1a4ea2718 100644 --- a/backend/danswer/one_shot_answer/answer_question.py +++ b/backend/danswer/one_shot_answer/answer_question.py @@ -181,8 +181,6 @@ def stream_answer_objects( max_tokens=max_document_tokens, use_sections=query_req.chunks_above > 0 or query_req.chunks_below > 0, ) - print("EVALLLUATINO") - print(query_req.evaluation_type) search_tool = SearchTool( db_session=db_session, diff --git a/backend/danswer/search/pipeline.py b/backend/danswer/search/pipeline.py index 7767f31d7..5c8067d96 100644 --- a/backend/danswer/search/pipeline.py +++ b/backend/danswer/search/pipeline.py @@ -370,8 +370,13 @@ class SearchPipeline: ) for section in sections ] - results = run_functions_in_parallel(function_calls=functions) - self._section_relevance = list(results.values()) + try: + results = run_functions_in_parallel(function_calls=functions) + self._section_relevance = list(results.values()) + except Exception: + raise ValueError( + "An issue occured during the agentic evaluation proecss." + ) elif self.search_query.evaluation_type == LLMEvaluationType.BASIC: if DISABLE_LLM_DOC_RELEVANCE: diff --git a/backend/danswer/server/documents/connector.py b/backend/danswer/server/documents/connector.py index d9f6d47dd..dd17ceab6 100644 --- a/backend/danswer/server/documents/connector.py +++ b/backend/danswer/server/documents/connector.py @@ -74,6 +74,7 @@ from danswer.file_store.file_store import get_default_file_store from danswer.server.documents.models import AuthStatus from danswer.server.documents.models import AuthUrl from danswer.server.documents.models import ConnectorBase +from danswer.server.documents.models import ConnectorCredentialBase from danswer.server.documents.models import ConnectorCredentialPairIdentifier from danswer.server.documents.models import ConnectorIndexingStatus from danswer.server.documents.models import ConnectorSnapshot @@ -504,7 +505,7 @@ def create_connector_from_model( @router.post("/admin/connector-with-mock-credential") def create_connector_with_mock_credential( - connector_data: ConnectorBase, + connector_data: ConnectorCredentialBase, user: User = Depends(current_admin_user), db_session: Session = Depends(get_session), ) -> StatusResponse: @@ -520,7 +521,7 @@ def create_connector_with_mock_credential( response = add_credential_to_connector( connector_id=cast(int, connector_response.id), # will aways be an int credential_id=credential.id, - is_public=True, + is_public=connector_data.is_public, user=user, db_session=db_session, cc_pair_name=connector_data.name, diff --git a/backend/danswer/server/documents/models.py b/backend/danswer/server/documents/models.py index 7486b693e..822f7c38d 100644 --- a/backend/danswer/server/documents/models.py +++ b/backend/danswer/server/documents/models.py @@ -43,6 +43,10 @@ class ConnectorBase(BaseModel): indexing_start: datetime | None +class ConnectorCredentialBase(ConnectorBase): + is_public: bool + + class ConnectorSnapshot(ConnectorBase): id: int credential_ids: list[int] diff --git a/backend/danswer/tools/search/search_tool.py b/backend/danswer/tools/search/search_tool.py index 66c85bbec..5a44f3761 100644 --- a/backend/danswer/tools/search/search_tool.py +++ b/backend/danswer/tools/search/search_tool.py @@ -181,7 +181,7 @@ class SearchTool(Tool): self, query: str ) -> Generator[ToolResponse, None, None]: if self.selected_sections is None: - raise ValueError("sections must be specified") + raise ValueError("Sections must be specified") yield ToolResponse( id=SEARCH_RESPONSE_SUMMARY_ID, diff --git a/web/src/app/admin/add-connector/page.tsx b/web/src/app/admin/add-connector/page.tsx index dfb937494..bf7032b5f 100644 --- a/web/src/app/admin/add-connector/page.tsx +++ b/web/src/app/admin/add-connector/page.tsx @@ -4,7 +4,7 @@ import { AdminPageTitle } from "@/components/admin/Title"; import { ConnectorIcon } from "@/components/icons/icons"; import { SourceCategory, SourceMetadata } from "@/lib/search/interfaces"; import { listSourceMetadata } from "@/lib/sources"; -import { Title, Text } from "@tremor/react"; +import { Title, Text, Button } from "@tremor/react"; import Link from "next/link"; import { useEffect, useMemo, useRef, useState } from "react"; @@ -96,6 +96,13 @@ export default function Page() { } title="Add Connector" + farRightElement={ + + + + } /> { + if (seconds === null) return "-"; + const minutes = Math.round(seconds / 60); + return `${minutes} minute${minutes !== 1 ? "s" : ""}`; + }; + + const formatDate = (date: Date | null): string => { + if (date === null) return "-"; + return date.toLocaleString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + timeZoneName: "short", + }); + }; + + return ( + <> + Advanced Configuration + + + {pruneFreq && ( + + Pruning Frequency + {formatFrequency(pruneFreq)} + + )} + {refreshFreq && ( + + Refresh Frequency + {formatFrequency(refreshFreq)} + + )} + {indexingStart && ( + + Indexing Start + {formatDate(indexingStart)} + + )} + + + + ); +} + export function ConfigDisplay({ connectorSpecificConfig, sourceType, diff --git a/web/src/app/admin/connector/[ccPairId]/page.tsx b/web/src/app/admin/connector/[ccPairId]/page.tsx index 30c2a4790..4bb82b8ae 100644 --- a/web/src/app/admin/connector/[ccPairId]/page.tsx +++ b/web/src/app/admin/connector/[ccPairId]/page.tsx @@ -6,7 +6,7 @@ import { CCPairStatus } from "@/components/Status"; import { BackButton } from "@/components/BackButton"; import { Button, Divider, Title } from "@tremor/react"; import { IndexingAttemptsTable } from "./IndexingAttemptsTable"; -import { ConfigDisplay } from "./ConfigDisplay"; +import { AdvancedConfigDisplay, ConfigDisplay } from "./ConfigDisplay"; import { ModifyStatusButtonCluster } from "./ModifyStatusButtonCluster"; import { DeletionButton } from "./DeletionButton"; import { ErrorCallout } from "@/components/ErrorCallout"; @@ -120,6 +120,12 @@ function Main({ ccPairId }: { ccPairId: number }) { setIsEditing(false); setEditableName(ccPair.name); }; + + const { + prune_freq: pruneFreq, + refresh_freq: refreshFreq, + indexing_start: indexingStart, + } = ccPair.connector; return ( <> {popup} @@ -197,6 +203,15 @@ function Main({ ccPairId }: { ccPairId: number }) { connectorSpecificConfig={ccPair.connector.connector_specific_config} sourceType={ccPair.connector.source} /> + + {(pruneFreq || indexingStart || refreshFreq) && ( + + )} + {/* NOTE: no divider / title here for `ConfigDisplay` since it is optional and we need to render these conditionally.*/}
diff --git a/web/src/app/admin/connectors/[connector]/AddConnectorPage.tsx b/web/src/app/admin/connectors/[connector]/AddConnectorPage.tsx index 404d243e8..c99759d7f 100644 --- a/web/src/app/admin/connectors/[connector]/AddConnectorPage.tsx +++ b/web/src/app/admin/connectors/[connector]/AddConnectorPage.tsx @@ -192,7 +192,8 @@ export default function AddConnector({ disabled: false, }, undefined, - credentialActivated ? false : true + credentialActivated ? false : true, + isPublic ); // If no credential @@ -328,7 +329,6 @@ export default function AddConnector({ <> Select a credential -
diff --git a/web/src/app/chat/modal/configuration/AssistantsTab.tsx b/web/src/app/chat/modal/configuration/AssistantsTab.tsx index d286f53bf..c03796cca 100644 --- a/web/src/app/chat/modal/configuration/AssistantsTab.tsx +++ b/web/src/app/chat/modal/configuration/AssistantsTab.tsx @@ -74,7 +74,7 @@ export function AssistantsTab({ items={assistants.map((a) => a.id.toString())} strategy={verticalListSortingStrategy} > -
+
{assistants.map((assistant) => ( ( connector: ConnectorBase, connectorId?: number, - fake_credential?: boolean + fakeCredential?: boolean, + isPublicCcpair?: boolean // exclusively for mock credentials, when also need to specify ccpair details ): Promise<{ message: string; isSuccess: boolean; response?: Connector }> { const isUpdate = connectorId !== undefined; if (!connector.connector_specific_config) { @@ -28,7 +29,7 @@ export async function submitConnector( } try { - if (fake_credential) { + if (fakeCredential) { const response = await fetch( "/api/manage/admin/connector-with-mock-credential", { @@ -36,7 +37,7 @@ export async function submitConnector( headers: { "Content-Type": "application/json", }, - body: JSON.stringify(connector), + body: JSON.stringify({ ...connector, is_public: isPublicCcpair }), } ); if (response.ok) { diff --git a/web/src/components/assistants/AssistantCards.tsx b/web/src/components/assistants/AssistantCards.tsx index ec71e5951..60c6ee248 100644 --- a/web/src/components/assistants/AssistantCards.tsx +++ b/web/src/components/assistants/AssistantCards.tsx @@ -33,17 +33,17 @@ export const AssistantCard = ({ shadow-md rounded-lg border-border - max-w-full + grow flex items-center - overflow-x-hidden + overflow-hidden `} onMouseEnter={() => setHovering(true)} onMouseLeave={() => setHovering(false)} > -
+
-
+
{assistant.name}
@@ -102,13 +102,16 @@ export function DraggableAssistantCard(props: { }; return ( -
+
-
- -
+ +
); } diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index 992d30f15..a36b0a344 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -599,6 +599,75 @@ export const BackIcon = ({ ); }; +export const MagnifyingIcon = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => { + return ( + + + + ); +}; + +export const ToggleDown = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => { + return ( + + + + ); +}; + +export const ToggleUp = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => { + return ( + + + + ); +}; + export const BroomIcon = ({ size = 16, className = defaultTailwindCSS, diff --git a/web/src/components/search/DocumentDisplay.tsx b/web/src/components/search/DocumentDisplay.tsx index 44d53a241..1b80c8a0d 100644 --- a/web/src/components/search/DocumentDisplay.tsx +++ b/web/src/components/search/DocumentDisplay.tsx @@ -18,6 +18,7 @@ import { FaStar } from "react-icons/fa"; import { FiTag } from "react-icons/fi"; import { DISABLE_LLM_DOC_RELEVANCE } from "@/lib/constants"; import { SettingsContext } from "../settings/SettingsProvider"; +import { CustomTooltip, TooltipGroup } from "../tooltip/CustomTooltip"; export const buildDocumentSummaryDisplay = ( matchHighlights: string[], @@ -227,30 +228,33 @@ export const DocumentDisplay = ({

- {isHovered && messageId && ( - - )} - - {(contentEnriched || additional_relevance) && - relevance_explanation && - (isHovered || alternativeToggled || settings?.isMobile) && ( - + + {isHovered && messageId && ( + )} + {(contentEnriched || additional_relevance) && + relevance_explanation && + (isHovered || alternativeToggled || settings?.isMobile) && ( + + )} +
@@ -339,7 +343,9 @@ export const AgenticDocumentDisplay = ({ ) } > - + + + )}
diff --git a/web/src/components/search/DocumentFeedbackBlock.tsx b/web/src/components/search/DocumentFeedbackBlock.tsx index 470bd402c..9d61e40b8 100644 --- a/web/src/components/search/DocumentFeedbackBlock.tsx +++ b/web/src/components/search/DocumentFeedbackBlock.tsx @@ -7,6 +7,7 @@ import { LightBulbIcon, LightSettingsIcon, } from "../icons/icons"; +import { CustomTooltip } from "../tooltip/CustomTooltip"; type DocumentFeedbackType = "endorse" | "reject" | "hide" | "unhide"; @@ -115,21 +116,24 @@ export const DocumentFeedbackBlock = ({ }: DocumentFeedbackBlockProps) => { return (
- - - + + + + + +
); }; diff --git a/web/src/components/search/QAFeedback.tsx b/web/src/components/search/QAFeedback.tsx index 47621c233..8f65e1342 100644 --- a/web/src/components/search/QAFeedback.tsx +++ b/web/src/components/search/QAFeedback.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { PopupSpec } from "../admin/connectors/Popup"; import { ThumbsDownIcon, ThumbsUpIcon } from "../icons/icons"; +import { CustomTooltip } from "../tooltip/CustomTooltip"; type Feedback = "like" | "dislike"; @@ -55,15 +56,9 @@ const QAFeedback = ({ className={"cursor-pointer " + paddingY} > {feedbackType === "like" ? ( - + ) : ( - + )}
); @@ -80,17 +75,22 @@ export const QAFeedbackBlock = ({ }: QAFeedbackBlockProps) => { return (
- -
+ + + +
+ + +
); diff --git a/web/src/components/search/SearchResultsDisplay.tsx b/web/src/components/search/SearchResultsDisplay.tsx index 4dea7478c..b5dea7350 100644 --- a/web/src/components/search/SearchResultsDisplay.tsx +++ b/web/src/components/search/SearchResultsDisplay.tsx @@ -7,7 +7,7 @@ import { SearchResponse, } from "@/lib/search/interfaces"; import { usePopup } from "../admin/connectors/Popup"; -import { AlertIcon, BroomIcon, UndoIcon } from "../icons/icons"; +import { AlertIcon, BroomIcon, MagnifyingIcon, UndoIcon } from "../icons/icons"; import { AgenticDocumentDisplay, DocumentDisplay } from "./DocumentDisplay"; import { searchState } from "./SearchSection"; import { useContext, useEffect, useState } from "react"; @@ -210,7 +210,7 @@ export const SearchResultsDisplay = ({ {!sweep ? ( - + ) : ( )} diff --git a/web/src/components/search/SearchSection.tsx b/web/src/components/search/SearchSection.tsx index 7da591077..523fcdd3c 100644 --- a/web/src/components/search/SearchSection.tsx +++ b/web/src/components/search/SearchSection.tsx @@ -35,6 +35,18 @@ import { AnswerSection } from "./results/AnswerSection"; import { QuotesSection } from "./results/QuotesSection"; import { QAFeedbackBlock } from "./QAFeedback"; import { usePopup } from "../admin/connectors/Popup"; +import { ToggleRight } from "@phosphor-icons/react"; +import { + DislikeFeedbackIcon, + LikeFeedbackIcon, + ToggleDown, + ToggleUp, +} from "../icons/icons"; +import { CustomTooltip, TooltipGroup } from "../tooltip/CustomTooltip"; +import { HoverableIcon } from "../Hoverable"; +import { FeedbackType } from "@/app/chat/types"; +import { FeedbackModal } from "@/app/chat/modal/FeedbackModal"; +import { handleChatFeedback } from "@/app/chat/lib"; export type searchState = | "input" @@ -99,6 +111,7 @@ export const SearchSection = ({ }); const [agentic, setAgentic] = useState(agenticSearchEnabled); + const [searchAnswerExpanded, setSearchAnswerExpanded] = useState(false); const toggleAgentic = () => { Cookies.set( @@ -148,6 +161,9 @@ export const SearchSection = ({ personas[0]?.id || 0 ); + // Used for search state display + const [analyzeStartTime, setAnalyzeStartTime] = useState(0); + // Filters const filterManager = useFilters(); const availableSources = ccPairs.map((ccPair) => ccPair.source); @@ -218,6 +234,16 @@ export const SearchSection = ({ const [defaultOverrides, setDefaultOverrides] = useState(SEARCH_DEFAULT_OVERRIDES_START); + const newSearchState = ( + currentSearchState: searchState, + newSearchState: searchState + ) => { + if (currentSearchState != "input") { + return newSearchState; + } + return "input"; + }; + // Helpers const initialSearchResponse: SearchResponse = { answer: null, @@ -237,12 +263,15 @@ export const SearchSection = ({ answer, })); - setSearchState((searchState) => { - if (searchState != "input") { - return "generating"; - } - return "input"; - }); + if (analyzeStartTime) { + const elapsedTime = Date.now() - analyzeStartTime; + const nextInterval = Math.ceil(elapsedTime / 1500) * 1500; + setTimeout(() => { + setSearchState((searchState) => + newSearchState(searchState, "generating") + ); + }, nextInterval - elapsedTime); + } }; const updateQuotes = (quotes: Quote[]) => { @@ -256,20 +285,17 @@ export const SearchSection = ({ const updateDocs = (documents: SearchDanswerDocument[]) => { if (agentic) { setTimeout(() => { - setSearchState((searchState) => { - if (searchState != "input") { - return "reading"; - } - return "input"; - }); + setSearchState((searchState) => newSearchState(searchState, "reading")); }, 1500); setTimeout(() => { + setAnalyzeStartTime(Date.now()); setSearchState((searchState) => { - if (searchState != "input") { - return "analyzing"; + const newState = newSearchState(searchState, "analyzing"); + if (newState === "analyzing") { + setAnalyzeStartTime(Date.now()); } - return "input"; + return newState; }); }, 4500); } @@ -301,11 +327,14 @@ export const SearchSection = ({ ...(prevState || initialSearchResponse), selectedDocIndices: docIndices, })); - const updateError = (error: FlowType) => + const updateError = (error: FlowType) => { + resetInput(true); + setSearchResponse((prevState) => ({ ...(prevState || initialSearchResponse), error, })); + }; const updateMessageAndThreadId = ( messageId: number, chat_session_id: number @@ -348,11 +377,12 @@ export const SearchSection = ({ } }; - const resetInput = () => { + const resetInput = (finalized?: boolean) => { setSweep(false); setFirstSearch(false); setComments(null); - setSearchState("searching"); + setSearchState(finalized ? "input" : "searching"); + setSearchAnswerExpanded(false); }; const [agenticResults, setAgenticResults] = useState(null); @@ -429,7 +459,6 @@ export const SearchSection = ({ cancellationToken: lastSearchCancellationToken.current, fn: updateDocumentRelevance, }), - updateComments: cancellable({ cancellationToken: lastSearchCancellationToken.current, fn: updateComments, @@ -513,6 +542,63 @@ export const SearchSection = ({ } }); } + const [currentFeedback, setCurrentFeedback] = useState< + [FeedbackType, number] | null + >(null); + + // + const [searchAnswerOverflowing, setSearchAnswerOverflowing] = useState(false); + const answerContainerRef = useRef(null); + + const handleFeedback = (feedbackType: FeedbackType, messageId: number) => { + setCurrentFeedback([feedbackType, messageId]); + }; + + useEffect(() => { + const checkOverflow = () => { + if (answerContainerRef.current) { + const isOverflowing = + answerContainerRef.current.scrollHeight > + answerContainerRef.current.clientHeight; + setSearchAnswerOverflowing(isOverflowing); + } + }; + + checkOverflow(); + window.addEventListener("resize", checkOverflow); + + return () => { + window.removeEventListener("resize", checkOverflow); + }; + }, [answer]); + + const onFeedback = async ( + messageId: number, + feedbackType: FeedbackType, + feedbackDetails: string, + predefinedFeedback: string | undefined + ) => { + const response = await handleChatFeedback( + messageId, + feedbackType, + feedbackDetails, + predefinedFeedback + ); + + if (response.ok) { + setPopup({ + message: "Thanks for your feedback!", + type: "success", + }); + } else { + const responseJson = await response.json(); + const errorMsg = responseJson.detail || responseJson.message; + setPopup({ + message: `Failed to submit feedback - ${errorMsg}`, + type: "error", + }); + } + }; const chatBannerPresent = settings?.enterpriseSettings?.custom_header_content; @@ -521,6 +607,22 @@ export const SearchSection = ({ return ( <>
+ {popup} + {currentFeedback && ( + setCurrentFeedback(null)} + onSubmit={({ message, predefinedFeedback }) => { + onFeedback( + currentFeedback[1], + currentFeedback[0], + message, + predefinedFeedback + ); + setCurrentFeedback(null); + }} + /> + )}
{!firstSearch && ( -
+
-
+

AI Answer

@@ -680,7 +785,7 @@ export const SearchSection = ({ className="relative inline-block" > - Generating citations... + Creating citations...
)} @@ -712,14 +817,16 @@ export const SearchSection = ({ className="relative inline-block" > - Generating + Running {settings?.isMobile ? "" : " Analysis"}...
)}
-
+
- {quotes !== null && quotes.length > 0 && answer && ( -
- + {searchAnswerExpanded || + (!searchAnswerOverflowing && ( +
+ {quotes !== null && + quotes.length > 0 && + answer && ( + + )} - {searchResponse.messageId !== null && ( -
- -
- )} -
- )} + {searchResponse.messageId !== null && ( +
+ } + onClick={() => + handleFeedback( + "like", + searchResponse?.messageId as number + ) + } + /> + } + onClick={() => + handleFeedback( + "dislike", + searchResponse?.messageId as number + ) + } + /> +
+ )} +
+ ))}
+ {!searchAnswerExpanded && searchAnswerOverflowing && ( +
+ )} + + {!searchAnswerExpanded && searchAnswerOverflowing && ( +
+ +
+ )}
)} diff --git a/web/src/components/search/results/QuotesSection.tsx b/web/src/components/search/results/QuotesSection.tsx index bf49f252e..16f1324f2 100644 --- a/web/src/components/search/results/QuotesSection.tsx +++ b/web/src/components/search/results/QuotesSection.tsx @@ -105,7 +105,9 @@ export const QuotesSection = (props: QuotesSectionProps) => { {}
+
+ {} +
} body={} desiredOpenStatus={true} diff --git a/web/src/components/search/results/ResponseSection.tsx b/web/src/components/search/results/ResponseSection.tsx index 30903ea79..2832c4626 100644 --- a/web/src/components/search/results/ResponseSection.tsx +++ b/web/src/components/search/results/ResponseSection.tsx @@ -71,17 +71,7 @@ export const ResponseSection = ({ }} >
{icon}
-
{header}
- - {!isNotControllable && ( -
- {finalIsOpen ? ( - - ) : ( - - )} -
- )} +
{header}
{finalIsOpen &&
{body}
}
diff --git a/web/src/lib/search/streamingQa.ts b/web/src/lib/search/streamingQa.ts index 51771222d..1f9b595c1 100644 --- a/web/src/lib/search/streamingQa.ts +++ b/web/src/lib/search/streamingQa.ts @@ -102,7 +102,6 @@ export const searchRequestStreamed = async ({ if (Object.hasOwn(chunk, "relevance_summaries")) { const relevanceChunk = chunk as RelevanceChunk; - const responseTaken = relevanceChunk.relevance_summaries; updateDocumentRelevance(relevanceChunk.relevance_summaries); } @@ -183,6 +182,18 @@ export const searchRequestStreamed = async ({ } } catch (err) { console.error("Fetch error:", err); + let errorMessage = "An error occurred while fetching the answer."; + + if (err instanceof Error) { + if (err.message.includes("rate_limit_error")) { + errorMessage = + "Rate limit exceeded. Please try again later or reduce the length of your query."; + } else { + errorMessage = err.message; + } + } + + updateError(errorMessage); } return { answer, quotes, relevantDocuments };