mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-04-09 20:39:29 +02:00
Admin UX updates (#2057)
This commit is contained in:
parent
eab82782ca
commit
c4e1c62c00
@ -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,
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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()}
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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" />
|
||||
)}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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 };
|
||||
|
Loading…
x
Reference in New Issue
Block a user