Admin UX updates (#2057)

This commit is contained in:
pablodanswer 2024-08-07 14:55:16 -07:00 committed by GitHub
parent eab82782ca
commit c4e1c62c00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 467 additions and 132 deletions

View File

@ -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,

View File

@ -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:

View File

@ -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,

View File

@ -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]

View File

@ -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,

View File

@ -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() {
<AdminPageTitle
icon={<ConnectorIcon size={32} />}
title="Add Connector"
farRightElement={
<Link href="/admin/indexing/status">
<Button color="green" size="xs">
See Connectors
</Button>
</Link>
}
/>
<input

View File

@ -38,6 +38,62 @@ function buildConfigEntries(
return obj;
}
export function AdvancedConfigDisplay({
pruneFreq,
refreshFreq,
indexingStart,
}: {
pruneFreq: number | null;
refreshFreq: number | null;
indexingStart: Date | null;
}) {
const formatFrequency = (seconds: number | null): string => {
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 (
<>
<Title className="mt-8 mb-2">Advanced Configuration</Title>
<Card>
<List>
{pruneFreq && (
<ListItem key={0}>
<span>Pruning Frequency</span>
<span>{formatFrequency(pruneFreq)}</span>
</ListItem>
)}
{refreshFreq && (
<ListItem key={1}>
<span>Refresh Frequency</span>
<span>{formatFrequency(refreshFreq)}</span>
</ListItem>
)}
{indexingStart && (
<ListItem key={2}>
<span>Indexing Start</span>
<span>{formatDate(indexingStart)}</span>
</ListItem>
)}
</List>
</Card>
</>
);
}
export function ConfigDisplay({
connectorSpecificConfig,
sourceType,

View File

@ -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) && (
<AdvancedConfigDisplay
pruneFreq={pruneFreq}
indexingStart={indexingStart}
refreshFreq={refreshFreq}
/>
)}
{/* NOTE: no divider / title here for `ConfigDisplay` since it is optional and we need
to render these conditionally.*/}
<div className="mt-6">

View File

@ -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({
<>
<Card>
<Title className="mb-2 text-lg">Select a credential</Title>
<GmailMain />
</Card>
<div className="mt-4 flex w-full justify-end">

View File

@ -74,7 +74,7 @@ export function AssistantsTab({
items={assistants.map((a) => a.id.toString())}
strategy={verticalListSortingStrategy}
>
<div className="px-2 pb-2 mx-2 max-h-[500px] miniscroll overflow-y-scroll my-3 grid grid-cols-1 gap-4">
<div className="px-4 pb-2 max-h-[500px] include-scrollbar overflow-y-scroll my-3 grid grid-cols-1 gap-4">
{assistants.map((assistant) => (
<DraggableAssistantCard
key={assistant.id.toString()}

View File

@ -17,6 +17,28 @@
}
}
.include-scrollbar::-webkit-scrollbar {
width: 6px;
}
.include-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
}
.include-scrollbar::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
.include-scrollbar::-webkit-scrollbar-thumb:hover {
background: #555;
}
.include-scrollbar {
scrollbar-width: thin;
scrollbar-color: #888 #f1f1f1;
}
.inputscroll::-webkit-scrollbar-track {
background: #e5e7eb;
scrollbar-width: none;

View File

@ -20,7 +20,8 @@ const BASE_CONNECTOR_URL = "/api/manage/admin/connector";
export async function submitConnector<T>(
connector: ConnectorBase<T>,
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<T> }> {
const isUpdate = connectorId !== undefined;
if (!connector.connector_specific_config) {
@ -28,7 +29,7 @@ export async function submitConnector<T>(
}
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<T>(
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(connector),
body: JSON.stringify({ ...connector, is_public: isPublicCcpair }),
}
);
if (response.ok) {

View File

@ -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)}
>
<div className="flex-grow">
<div className="w-full">
<div className="flex items-center mb-2">
<AssistantIcon assistant={assistant} />
<div className="ml-2 ellipsis font-bold text-sm text-emphasis">
<div className="ml-2 ellipsis truncate font-bold text-sm text-emphasis">
{assistant.name}
</div>
</div>
@ -102,13 +102,16 @@ export function DraggableAssistantCard(props: {
};
return (
<div ref={setNodeRef} style={style} className="flex items-center">
<div
ref={setNodeRef}
style={style}
className="overlow-y-scroll inputscroll flex items-center"
>
<div {...attributes} {...listeners} className="mr-1 cursor-grab">
<MdDragIndicator className="h-3 w-3 flex-none" />
</div>
<div className="flex-grow">
<AssistantCard {...props} />
</div>
<AssistantCard {...props} />
</div>
);
}

View File

@ -599,6 +599,75 @@ export const BackIcon = ({
);
};
export const MagnifyingIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
xmlns="http://www.w3.org/2000/svg"
width="200"
height="200"
viewBox="0 0 16 16"
>
<path
fill="currentColor"
fill-rule="evenodd"
d="M9.965 11.026a5 5 0 1 1 1.06-1.06l2.755 2.754a.75.75 0 1 1-1.06 1.06zM10.5 7a3.5 3.5 0 1 1-7 0a3.5 3.5 0 0 1 7 0"
clip-rule="evenodd"
/>
</svg>
);
};
export const ToggleDown = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
xmlns="http://www.w3.org/2000/svg"
width="200"
height="200"
viewBox="0 0 16 16"
>
<path
fill="currentColor"
fill-rule="evenodd"
d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06"
clip-rule="evenodd"
/>
</svg>
);
};
export const ToggleUp = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
xmlns="http://www.w3.org/2000/svg"
width="200"
height="200"
viewBox="0 0 16 16"
>
<path
fill="currentColor"
fill-rule="evenodd"
d="M11.78 9.78a.75.75 0 0 1-1.06 0L8 7.06L5.28 9.78a.75.75 0 0 1-1.06-1.06l3.25-3.25a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06"
clip-rule="evenodd"
/>
</svg>
);
};
export const BroomIcon = ({
size = 16,
className = defaultTailwindCSS,

View File

@ -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 = ({
</p>
</a>
<div className="ml-auto flex gap-x-2">
{isHovered && messageId && (
<DocumentFeedbackBlock
documentId={document.document_id}
messageId={messageId}
documentRank={documentRank}
setPopup={setPopup}
/>
)}
{(contentEnriched || additional_relevance) &&
relevance_explanation &&
(isHovered || alternativeToggled || settings?.isMobile) && (
<button
onClick={() =>
setAlternativeToggled(
(alternativeToggled) => !alternativeToggled
)
}
>
<LightBulbIcon
className={`${settings?.isMobile && alternativeToggled ? "text-green-600" : "text-blue-600"} h-4 w-4 cursor-pointer`}
/>
</button>
<TooltipGroup>
{isHovered && messageId && (
<DocumentFeedbackBlock
documentId={document.document_id}
messageId={messageId}
documentRank={documentRank}
setPopup={setPopup}
/>
)}
{(contentEnriched || additional_relevance) &&
relevance_explanation &&
(isHovered || alternativeToggled || settings?.isMobile) && (
<button
onClick={() =>
setAlternativeToggled(
(alternativeToggled) => !alternativeToggled
)
}
>
<CustomTooltip showTick line content="Toggle content">
<LightBulbIcon
className={`${settings?.isMobile && alternativeToggled ? "text-green-600" : "text-blue-600"} h-4 w-4 cursor-pointer`}
/>
</CustomTooltip>
</button>
)}
</TooltipGroup>
</div>
</div>
<div className="mt-1">
@ -339,7 +343,9 @@ export const AgenticDocumentDisplay = ({
)
}
>
<BookIcon className="text-blue-400" />
<CustomTooltip showTick line content="Toggle content">
<BookIcon className="text-blue-400" />
</CustomTooltip>
</button>
)}
</div>

View File

@ -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 (
<div className="flex items-center gap-x-2">
<DocumentFeedback
documentId={documentId}
messageId={messageId}
documentRank={documentRank}
setPopup={setPopup}
feedbackType="endorse"
/>
<DocumentFeedback
documentId={documentId}
messageId={messageId}
documentRank={documentRank}
setPopup={setPopup}
feedbackType="reject"
/>
<CustomTooltip showTick line content="Good response">
<DocumentFeedback
documentId={documentId}
messageId={messageId}
documentRank={documentRank}
setPopup={setPopup}
feedbackType="endorse"
/>
</CustomTooltip>
<CustomTooltip showTick line content="Bad response">
<DocumentFeedback
documentId={documentId}
messageId={messageId}
documentRank={documentRank}
setPopup={setPopup}
feedbackType="reject"
/>
</CustomTooltip>
</div>
);
};

View File

@ -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" ? (
<ThumbsUpIcon
size={size}
className="my-auto flex flex-shrink-0 text-gray-500"
/>
<ThumbsUpIcon className="my-auto flex flex-shrink-0 text-gray-500" />
) : (
<ThumbsDownIcon
size={size}
className="my-auto flex flex-shrink-0 text-gray-500"
/>
<ThumbsDownIcon className="my-auto flex flex-shrink-0 text-gray-500" />
)}
</div>
);
@ -80,17 +75,22 @@ export const QAFeedbackBlock = ({
}: QAFeedbackBlockProps) => {
return (
<div className="flex">
<QAFeedback
messageId={messageId}
setPopup={setPopup}
feedbackType="like"
/>
<div className="ml-2">
<CustomTooltip line position="top" content="Like Search Response">
<QAFeedback
messageId={messageId}
setPopup={setPopup}
feedbackType="dislike"
feedbackType="like"
/>
</CustomTooltip>
<div className="ml-2">
<CustomTooltip line position="top" content="Dislike Search Response">
<QAFeedback
messageId={messageId}
setPopup={setPopup}
feedbackType="dislike"
/>
</CustomTooltip>
</div>
</div>
);

View File

@ -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 = ({
<span className="ml-1">
{!sweep ? (
<BroomIcon className="h-4 w-4" />
<MagnifyingIcon className="h-4 w-4" />
) : (
<UndoIcon className="h-4 w-4" />
)}

View File

@ -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<number>(0);
// Filters
const filterManager = useFilters();
const availableSources = ccPairs.map((ccPair) => ccPair.source);
@ -218,6 +234,16 @@ export const SearchSection = ({
const [defaultOverrides, setDefaultOverrides] =
useState<SearchDefaultOverrides>(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<boolean | null>(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<HTMLDivElement>(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 (
<>
<div className="flex relative w-full pr-[8px] h-full text-default">
{popup}
{currentFeedback && (
<FeedbackModal
feedbackType={currentFeedback[0]}
onClose={() => setCurrentFeedback(null)}
onSubmit={({ message, predefinedFeedback }) => {
onFeedback(
currentFeedback[1],
currentFeedback[0],
message,
predefinedFeedback
);
setCurrentFeedback(null);
}}
/>
)}
<div
ref={sidebarElementRef}
className={`
@ -656,9 +758,12 @@ export const SearchSection = ({
/>
</div>
{!firstSearch && (
<div className="my-4 min-h-[16rem] p-4 border-2 border-border rounded-lg relative">
<div
ref={answerContainerRef}
className={`my-4 ${searchAnswerExpanded ? "min-h-[16rem]" : "h-[16rem]"} overflow-y-hidden p-4 border-2 border-border rounded-lg relative`}
>
<div>
<div className="flex gap-x-2 mb-1">
<div className="flex gap-x-2">
<h2 className="text-emphasis font-bold my-auto mb-1 ">
AI Answer
</h2>
@ -680,7 +785,7 @@ export const SearchSection = ({
className="relative inline-block"
>
<span className="loading-text">
Generating citations...
Creating citations...
</span>
</div>
)}
@ -712,14 +817,16 @@ export const SearchSection = ({
className="relative inline-block"
>
<span className="loading-text">
Generating
Running
{settings?.isMobile ? "" : " Analysis"}...
</span>
</div>
)}
</div>
<div className="mb-2 pt-1 border-t border-border w-full">
<div
className={`pt-1 h-auto border-t border-border w-full`}
>
<AnswerSection
answer={answer}
quotes={quotes}
@ -728,24 +835,58 @@ export const SearchSection = ({
/>
</div>
{quotes !== null && quotes.length > 0 && answer && (
<div className="pt-1 border-t border-border w-full">
<QuotesSection
quotes={dedupedQuotes}
isFetching={isFetching}
/>
{searchAnswerExpanded ||
(!searchAnswerOverflowing && (
<div className="w-full">
{quotes !== null &&
quotes.length > 0 &&
answer && (
<QuotesSection
quotes={dedupedQuotes}
isFetching={isFetching}
/>
)}
{searchResponse.messageId !== null && (
<div className="absolute right-3 bottom-3">
<QAFeedbackBlock
messageId={searchResponse.messageId}
setPopup={setPopup}
/>
</div>
)}
</div>
)}
{searchResponse.messageId !== null && (
<div className="absolute right-3 flex bottom-3">
<HoverableIcon
icon={<LikeFeedbackIcon />}
onClick={() =>
handleFeedback(
"like",
searchResponse?.messageId as number
)
}
/>
<HoverableIcon
icon={<DislikeFeedbackIcon />}
onClick={() =>
handleFeedback(
"dislike",
searchResponse?.messageId as number
)
}
/>
</div>
)}
</div>
))}
</div>
{!searchAnswerExpanded && searchAnswerOverflowing && (
<div className="absolute bottom-0 left-0 w-full h-[100px] bg-gradient-to-b from-background/5 via-background/60 to-background/90"></div>
)}
{!searchAnswerExpanded && searchAnswerOverflowing && (
<div className="w-full h-12 absolute items-center content-center flex left-0 px-4 bottom-0">
<button
onClick={() => setSearchAnswerExpanded(true)}
className="flex gap-x-1 items-center justify-center hover:bg-background-100 cursor-pointer max-w-sm text-sm mx-auto w-full bg-background border py-2 rounded-full"
>
Show more
<ToggleDown />
</button>
</div>
)}
</div>
)}

View File

@ -105,7 +105,9 @@ export const QuotesSection = (props: QuotesSectionProps) => {
<ResponseSection
status={status}
header={
<div className="ml-2 text-emphasis">{<QuotesHeader {...props} />}</div>
<div className="ml-2 text-emphasis font-bold">
{<QuotesHeader {...props} />}
</div>
}
body={<QuotesBody {...props} />}
desiredOpenStatus={true}

View File

@ -71,17 +71,7 @@ export const ResponseSection = ({
}}
>
<div className="my-auto">{icon}</div>
<div className="my-auto text-sm text-gray-200 italic">{header}</div>
{!isNotControllable && (
<div className="ml-auto">
{finalIsOpen ? (
<ChevronDownIcon size={16} className="text-gray-400" />
) : (
<ChevronLeftIcon size={16} className="text-gray-400" />
)}
</div>
)}
<div className="my-auto text-sm text-gray-200">{header}</div>
</div>
{finalIsOpen && <div className="pb-1 mx-2 text-sm mb-1">{body}</div>}
</div>

View File

@ -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 };