mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-21 14:12:42 +02:00
Add answers to search (#2020)
This commit is contained in:
@@ -94,6 +94,7 @@ const AssistantCard = ({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-subtle">
|
||||
<span className="font-semibold">Default model:</span>{" "}
|
||||
{getDisplayNameForModel(
|
||||
|
@@ -36,7 +36,7 @@ import ToggleSearch from "./WrappedSearch";
|
||||
import {
|
||||
AGENTIC_SEARCH_TYPE_COOKIE_NAME,
|
||||
NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN,
|
||||
DISABLE_AGENTIC_SEARCH,
|
||||
DISABLE_LLM_DOC_RELEVANCE,
|
||||
} from "@/lib/constants";
|
||||
import WrappedSearch from "./WrappedSearch";
|
||||
|
||||
@@ -206,7 +206,7 @@ export default async function Home() {
|
||||
|
||||
<InstantSSRAutoRefresh />
|
||||
<WrappedSearch
|
||||
disabledAgentic={DISABLE_AGENTIC_SEARCH}
|
||||
disabledAgentic={DISABLE_LLM_DOC_RELEVANCE}
|
||||
initiallyToggled={toggleSidebar}
|
||||
querySessions={querySessions}
|
||||
user={user}
|
||||
|
@@ -1711,24 +1711,9 @@ export const ThumbsUpIcon = ({
|
||||
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 20 20"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M10 2c-2.236 0-4.43.18-6.57.524C1.993 2.755 1 4.014 1 5.426v5.148c0 1.413.993 2.67 2.43 2.902c1.168.188 2.352.327 3.55.414c.28.02.521.18.642.413l1.713 3.293a.75.75 0 0 0 1.33 0l1.713-3.293a.783.783 0 0 1 .642-.413a41.102 41.102 0 0 0 3.55-.414c1.437-.231 2.43-1.49 2.43-2.902V5.426c0-1.413-.993-2.67-2.43-2.902A41.289 41.289 0 0 0 10 2ZM6.75 6a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Zm0 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
return <FiThumbsUp size={size} className={className} />;
|
||||
};
|
||||
|
||||
export const RobotIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
|
@@ -38,9 +38,11 @@ export const TODAY = "Today";
|
||||
export function DateRangeSelector({
|
||||
value,
|
||||
onValueChange,
|
||||
isHoritontal,
|
||||
}: {
|
||||
value: DateRangePickerValue | null;
|
||||
onValueChange: (value: DateRangePickerValue | null) => void;
|
||||
isHoritontal?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
@@ -106,6 +108,7 @@ export function DateRangeSelector({
|
||||
flex
|
||||
text-sm
|
||||
px-3
|
||||
line-clamp-1
|
||||
py-1.5
|
||||
rounded-lg
|
||||
border
|
||||
@@ -113,12 +116,16 @@ export function DateRangeSelector({
|
||||
cursor-pointer
|
||||
hover:bg-hover`}
|
||||
>
|
||||
<FiCalendar className="my-auto mr-2" />{" "}
|
||||
{value?.selectValue ? (
|
||||
<div className="text-emphasis">{value.selectValue}</div>
|
||||
) : (
|
||||
"Any time..."
|
||||
)}
|
||||
<FiCalendar className="flex-none my-auto mr-2" />{" "}
|
||||
<p className="line-clamp-1">
|
||||
{value?.selectValue ? (
|
||||
<div className="text-emphasis">{value.selectValue}</div>
|
||||
) : isHoritontal ? (
|
||||
"Date"
|
||||
) : (
|
||||
"Any time..."
|
||||
)}
|
||||
</p>
|
||||
{value?.selectValue ? (
|
||||
<div
|
||||
className="my-auto ml-auto p-0.5 rounded-full w-fit"
|
||||
|
@@ -16,7 +16,7 @@ import { BookIcon, CheckmarkIcon, LightBulbIcon, XIcon } from "../icons/icons";
|
||||
|
||||
import { FaStar } from "react-icons/fa";
|
||||
import { FiTag } from "react-icons/fi";
|
||||
import { DISABLE_AGENTIC_SEARCH } from "@/lib/constants";
|
||||
import { DISABLE_LLM_DOC_RELEVANCE } from "@/lib/constants";
|
||||
import { SettingsContext } from "../settings/SettingsProvider";
|
||||
|
||||
export const buildDocumentSummaryDisplay = (
|
||||
|
@@ -1,15 +1,18 @@
|
||||
import React, { KeyboardEvent, ChangeEvent, useContext } from "react";
|
||||
import { searchState } from "./SearchSection";
|
||||
|
||||
import { MagnifyingGlass } from "@phosphor-icons/react";
|
||||
|
||||
interface FullSearchBarProps {
|
||||
query: string;
|
||||
setQuery: (query: string) => void;
|
||||
onSearch: (fast?: boolean) => void;
|
||||
searchState?: searchState;
|
||||
agentic?: boolean;
|
||||
toggleAgentic?: () => void;
|
||||
ccPairs: CCPairBasicInfo[];
|
||||
documentSets: DocumentSet[];
|
||||
filterManager: any; // You might want to replace 'any' with a more specific type
|
||||
finalAvailableDocumentSets: DocumentSet[];
|
||||
finalAvailableSources: string[];
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
@@ -18,6 +21,9 @@ import { Divider } from "@tremor/react";
|
||||
import { CustomTooltip } from "../tooltip/CustomTooltip";
|
||||
import KeyboardSymbol from "@/lib/browserUtilities";
|
||||
import { SettingsContext } from "../settings/SettingsProvider";
|
||||
import { HorizontalSourceSelector, SourceSelector } from "./filtering/Filters";
|
||||
import { CCPairBasicInfo, DocumentSet, Tag } from "@/lib/types";
|
||||
import { SourceMetadata } from "@/lib/search/interfaces";
|
||||
|
||||
export const AnimatedToggle = ({
|
||||
isOn,
|
||||
@@ -116,12 +122,17 @@ export const AnimatedToggle = ({
|
||||
export default AnimatedToggle;
|
||||
|
||||
export const FullSearchBar = ({
|
||||
searchState,
|
||||
query,
|
||||
setQuery,
|
||||
onSearch,
|
||||
agentic,
|
||||
toggleAgentic,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
filterManager,
|
||||
finalAvailableDocumentSets,
|
||||
finalAvailableSources,
|
||||
tags,
|
||||
}: FullSearchBarProps) => {
|
||||
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const target = event.target;
|
||||
@@ -196,47 +207,44 @@ export const FullSearchBar = ({
|
||||
suppressContentEditableWarning={true}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end w-full items-center space-x-3 mr-12 px-4 pb-2">
|
||||
{searchState == "searching" && (
|
||||
<div key={"Reading"} className="mr-auto relative inline-block">
|
||||
<span className="loading-text">Searching...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchState == "reading" && (
|
||||
<div key={"Reading"} className="mr-auto relative inline-block">
|
||||
<span className="loading-text">
|
||||
Reading{settings?.isMobile ? "" : " Documents"}...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchState == "analyzing" && (
|
||||
<div key={"Generating"} className="mr-auto relative inline-block">
|
||||
<span className="loading-text">
|
||||
Generating{settings?.isMobile ? "" : " Analysis"}...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{toggleAgentic && (
|
||||
<AnimatedToggle isOn={agentic!} handleToggle={toggleAgentic} />
|
||||
)}
|
||||
|
||||
<div className="my-auto pl-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
onSearch(agentic);
|
||||
}}
|
||||
className="flex my-auto cursor-pointer"
|
||||
>
|
||||
<SendIcon
|
||||
size={28}
|
||||
className={`text-emphasis text-white p-1 rounded-full ${
|
||||
query ? "bg-background-800" : "bg-[#D7D7D7]"
|
||||
}`}
|
||||
<div
|
||||
className={`flex 2xl:justify-end justify-between w-full items-center space-x-3 px-4 pb-2`}
|
||||
>
|
||||
{/* <div className="absolute z-10 mobile:px-4 mobile:max-w-searchbar-max mobile:w-[90%] top-12 desktop:left-0 hidden 2xl:block mobile:left-1/2 mobile:transform mobile:-translate-x-1/2 desktop:w-52 3xl:w-64"> */}
|
||||
<div className="2xl:hidden">
|
||||
{(ccPairs.length > 0 || documentSets.length > 0) && (
|
||||
<HorizontalSourceSelector
|
||||
isHorizontal
|
||||
{...filterManager}
|
||||
showDocSidebar={false}
|
||||
availableDocumentSets={finalAvailableDocumentSets}
|
||||
existingSources={finalAvailableSources}
|
||||
availableTags={tags}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{/* ccPairs, documentSets, filterManager, finalAvailableDocumentSets, finalAvailableSources, tags */}
|
||||
{/* </div>/ */}
|
||||
<div className="flex my-auto gap-x-3">
|
||||
{toggleAgentic && (
|
||||
<AnimatedToggle isOn={agentic!} handleToggle={toggleAgentic} />
|
||||
)}
|
||||
|
||||
<div className="my-auto pl-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
onSearch(agentic);
|
||||
}}
|
||||
className="flex my-auto cursor-pointer"
|
||||
>
|
||||
<SendIcon
|
||||
size={28}
|
||||
className={`text-emphasis text-white p-1 rounded-full ${
|
||||
query ? "bg-background-800" : "bg-[#D7D7D7]"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-2.5 right-10"></div>
|
||||
|
@@ -1,24 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { removeDuplicateDocs } from "@/lib/documentUtils";
|
||||
import {
|
||||
DanswerDocument,
|
||||
DocumentRelevance,
|
||||
FlowType,
|
||||
Quote,
|
||||
Relevance,
|
||||
SearchDanswerDocument,
|
||||
SearchDefaultOverrides,
|
||||
SearchResponse,
|
||||
ValidQuestionResponse,
|
||||
} from "@/lib/search/interfaces";
|
||||
import { usePopup } from "../admin/connectors/Popup";
|
||||
import { AlertIcon, BroomIcon, UndoIcon } from "../icons/icons";
|
||||
import { AgenticDocumentDisplay, DocumentDisplay } from "./DocumentDisplay";
|
||||
import { searchState } from "./SearchSection";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { Tooltip } from "../tooltip/Tooltip";
|
||||
import KeyboardSymbol from "@/lib/browserUtilities";
|
||||
import { SettingsContext } from "../settings/SettingsProvider";
|
||||
|
||||
const getSelectedDocumentIds = (
|
||||
documents: SearchDanswerDocument[],
|
||||
@@ -135,31 +130,17 @@ export const SearchResultsDisplay = ({
|
||||
);
|
||||
}
|
||||
|
||||
const dedupedQuotes: Quote[] = [];
|
||||
const seen = new Set<string>();
|
||||
if (quotes) {
|
||||
quotes.forEach((quote) => {
|
||||
if (!seen.has(quote.document_id)) {
|
||||
dedupedQuotes.push(quote);
|
||||
seen.add(quote.document_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const selectedDocumentIds = getSelectedDocumentIds(
|
||||
documents || [],
|
||||
searchResponse.selectedDocIndices || []
|
||||
);
|
||||
|
||||
const relevantDocs = documents
|
||||
? documents.filter((doc) => {
|
||||
return (
|
||||
showAll ||
|
||||
(searchResponse &&
|
||||
searchResponse.additional_relevance &&
|
||||
searchResponse.additional_relevance[
|
||||
`${doc.document_id}-${doc.chunk_ind}`
|
||||
].relevant) ||
|
||||
searchResponse.additional_relevance[doc.document_id].relevant) ||
|
||||
doc.is_relevant
|
||||
);
|
||||
})
|
||||
@@ -183,6 +164,7 @@ export const SearchResultsDisplay = ({
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
|
||||
{documents && documents.length == 0 && (
|
||||
<p className="flex text-lg font-bold">
|
||||
No docs found! Ensure that you have enabled at least one connector
|
||||
@@ -248,9 +230,7 @@ export const SearchResultsDisplay = ({
|
||||
{uniqueDocuments.map((document, ind) => {
|
||||
const relevance: DocumentRelevance | null =
|
||||
searchResponse.additional_relevance
|
||||
? searchResponse.additional_relevance[
|
||||
`${document.document_id}-${document.chunk_ind}`
|
||||
]
|
||||
? searchResponse.additional_relevance[document.document_id]
|
||||
: null;
|
||||
|
||||
return agenticResults ? (
|
||||
|
@@ -17,13 +17,11 @@ import {
|
||||
SearchDanswerDocument,
|
||||
} from "@/lib/search/interfaces";
|
||||
import { searchRequestStreamed } from "@/lib/search/streamingQa";
|
||||
|
||||
import { CancellationToken, cancellable } from "@/lib/search/cancellable";
|
||||
import { useFilters, useObjectState } from "@/lib/hooks";
|
||||
import { questionValidationStreamed } from "@/lib/search/streamingQuestionValidation";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { computeAvailableFilters } from "@/lib/filters";
|
||||
import { redirect, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { SettingsContext } from "../settings/SettingsProvider";
|
||||
import { HistorySidebar } from "@/app/chat/sessionSidebar/HistorySidebar";
|
||||
import { ChatSession, SearchSession } from "@/app/chat/interfaces";
|
||||
@@ -33,13 +31,19 @@ import { SIDEBAR_TOGGLED_COOKIE_NAME } from "../resizable/constants";
|
||||
import { AGENTIC_SEARCH_TYPE_COOKIE_NAME } from "@/lib/constants";
|
||||
import Cookies from "js-cookie";
|
||||
import FixedLogo from "@/app/chat/shared_chat_search/FixedLogo";
|
||||
import { AnswerSection } from "./results/AnswerSection";
|
||||
import { QuotesSection } from "./results/QuotesSection";
|
||||
import { QAFeedbackBlock } from "./QAFeedback";
|
||||
import { usePopup } from "../admin/connectors/Popup";
|
||||
|
||||
export type searchState =
|
||||
| "input"
|
||||
| "searching"
|
||||
| "reading"
|
||||
| "analyzing"
|
||||
| "summarizing";
|
||||
| "summarizing"
|
||||
| "generating"
|
||||
| "citing";
|
||||
|
||||
const SEARCH_DEFAULT_OVERRIDES_START: SearchDefaultOverrides = {
|
||||
forceDisplayQA: false,
|
||||
@@ -48,7 +52,6 @@ const SEARCH_DEFAULT_OVERRIDES_START: SearchDefaultOverrides = {
|
||||
|
||||
const VALID_QUESTION_RESPONSE_DEFAULT: ValidQuestionResponse = {
|
||||
reasoning: null,
|
||||
answerable: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
@@ -223,35 +226,48 @@ export const SearchSection = ({
|
||||
additional_relevance: undefined,
|
||||
};
|
||||
// Streaming updates
|
||||
const updateCurrentAnswer = (answer: string) =>
|
||||
const updateCurrentAnswer = (answer: string) => {
|
||||
setSearchResponse((prevState) => ({
|
||||
...(prevState || initialSearchResponse),
|
||||
answer,
|
||||
}));
|
||||
const updateQuotes = (quotes: Quote[]) =>
|
||||
|
||||
setSearchState((searchState) => {
|
||||
if (searchState != "input") {
|
||||
return "generating";
|
||||
}
|
||||
return "input";
|
||||
});
|
||||
};
|
||||
|
||||
const updateQuotes = (quotes: Quote[]) => {
|
||||
setSearchResponse((prevState) => ({
|
||||
...(prevState || initialSearchResponse),
|
||||
quotes,
|
||||
}));
|
||||
setSearchState((searchState) => "input");
|
||||
};
|
||||
|
||||
const updateDocs = (documents: SearchDanswerDocument[]) => {
|
||||
setTimeout(() => {
|
||||
setSearchState((searchState) => {
|
||||
if (searchState != "input") {
|
||||
return "reading";
|
||||
}
|
||||
return "input";
|
||||
});
|
||||
}, 1500);
|
||||
if (agentic) {
|
||||
setTimeout(() => {
|
||||
setSearchState((searchState) => {
|
||||
if (searchState != "input") {
|
||||
return "reading";
|
||||
}
|
||||
return "input";
|
||||
});
|
||||
}, 1500);
|
||||
|
||||
setTimeout(() => {
|
||||
setSearchState((searchState) => {
|
||||
if (searchState != "input") {
|
||||
return "analyzing";
|
||||
}
|
||||
return "input";
|
||||
});
|
||||
}, 4500);
|
||||
setTimeout(() => {
|
||||
setSearchState((searchState) => {
|
||||
if (searchState != "input") {
|
||||
return "analyzing";
|
||||
}
|
||||
return "input";
|
||||
});
|
||||
}, 4500);
|
||||
}
|
||||
|
||||
setSearchResponse((prevState) => ({
|
||||
...(prevState || initialSearchResponse),
|
||||
@@ -294,8 +310,9 @@ export const SearchSection = ({
|
||||
messageId,
|
||||
}));
|
||||
router.refresh();
|
||||
setSearchState("input");
|
||||
// setSearchState("input");
|
||||
setIsFetching(false);
|
||||
setSearchState((searchState) => "input");
|
||||
|
||||
// router.replace(`/search?searchId=${chat_session_id}`);
|
||||
};
|
||||
@@ -309,7 +326,11 @@ export const SearchSection = ({
|
||||
setContentEnriched(true);
|
||||
|
||||
setIsFetching(false);
|
||||
setSearchState("input");
|
||||
if (disabledAgentic) {
|
||||
setSearchState("input");
|
||||
} else {
|
||||
setSearchState("analyzing");
|
||||
}
|
||||
};
|
||||
|
||||
const updateComments = (comments: any) => {
|
||||
@@ -317,7 +338,9 @@ export const SearchSection = ({
|
||||
};
|
||||
|
||||
const finishedSearching = () => {
|
||||
setSearchState("input");
|
||||
if (disabledAgentic) {
|
||||
setSearchState("input");
|
||||
}
|
||||
};
|
||||
|
||||
const resetInput = () => {
|
||||
@@ -414,15 +437,7 @@ export const SearchSection = ({
|
||||
offset: offset ?? defaultOverrides.offset,
|
||||
};
|
||||
|
||||
const questionValidationArgs = {
|
||||
query,
|
||||
update: setValidQuestionResponse,
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
searchRequestStreamed(searchFnArgs),
|
||||
questionValidationStreamed(questionValidationArgs),
|
||||
]);
|
||||
await Promise.all([searchRequestStreamed(searchFnArgs)]);
|
||||
};
|
||||
|
||||
// handle redirect if search page is disabled
|
||||
@@ -481,6 +496,20 @@ export const SearchSection = ({
|
||||
setShowDocSidebar,
|
||||
mobile: settings?.isMobile,
|
||||
});
|
||||
const { answer, quotes, documents, error, messageId } = searchResponse;
|
||||
|
||||
const dedupedQuotes: Quote[] = [];
|
||||
const seen = new Set<string>();
|
||||
if (quotes) {
|
||||
quotes.forEach((quote) => {
|
||||
if (!seen.has(quote.document_id)) {
|
||||
dedupedQuotes.push(quote);
|
||||
seen.add(quote.document_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -600,15 +629,113 @@ export const SearchSection = ({
|
||||
disabledAgentic ? undefined : toggleAgentic
|
||||
}
|
||||
agentic={agentic}
|
||||
searchState={searchState}
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
onSearch={async (agentic?: boolean) => {
|
||||
setDefaultOverrides(SEARCH_DEFAULT_OVERRIDES_START);
|
||||
await onSearch({ agentic, offset: 0 });
|
||||
}}
|
||||
finalAvailableDocumentSets={finalAvailableDocumentSets}
|
||||
finalAvailableSources={finalAvailableSources}
|
||||
filterManager={filterManager}
|
||||
documentSets={documentSets}
|
||||
ccPairs={ccPairs}
|
||||
tags={tags}
|
||||
/>
|
||||
</div>
|
||||
{!firstSearch && (
|
||||
<div className="my-4 min-h-[16rem] p-4 border-2 border-border rounded-lg relative">
|
||||
<div>
|
||||
<div className="flex gap-x-2 mb-1">
|
||||
<h2 className="text-emphasis font-bold my-auto mb-1 ">
|
||||
AI Answer
|
||||
</h2>
|
||||
|
||||
{searchState == "generating" && (
|
||||
<div
|
||||
key={"generating"}
|
||||
className="relative inline-block"
|
||||
>
|
||||
<span className="loading-text">
|
||||
Generating response...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchState == "citing" && (
|
||||
<div
|
||||
key={"citing"}
|
||||
className="relative inline-block"
|
||||
>
|
||||
<span className="loading-text">
|
||||
Generating citations...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchState == "searching" && (
|
||||
<div
|
||||
key={"Reading"}
|
||||
className="relative inline-block"
|
||||
>
|
||||
<span className="loading-text">Searching...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchState == "reading" && (
|
||||
<div
|
||||
key={"Reading"}
|
||||
className="relative inline-block"
|
||||
>
|
||||
<span className="loading-text">
|
||||
Reading{settings?.isMobile ? "" : " Documents"}
|
||||
...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchState == "analyzing" && (
|
||||
<div
|
||||
key={"Generating"}
|
||||
className="relative inline-block"
|
||||
>
|
||||
<span className="loading-text">
|
||||
Generating
|
||||
{settings?.isMobile ? "" : " Analysis"}...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-2 pt-1 border-t border-border w-full">
|
||||
<AnswerSection
|
||||
answer={answer}
|
||||
quotes={quotes}
|
||||
error={error}
|
||||
isFetching={isFetching}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{quotes !== null && answer && (
|
||||
<div className="pt-1 border-t border-border w-full">
|
||||
<QuotesSection
|
||||
quotes={dedupedQuotes}
|
||||
isFetching={isFetching}
|
||||
/>
|
||||
|
||||
{searchResponse.messageId !== null && (
|
||||
<div className="absolute right-3 bottom-3">
|
||||
<QAFeedbackBlock
|
||||
messageId={searchResponse.messageId}
|
||||
setPopup={setPopup}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!settings?.isMobile && (
|
||||
<div className="mt-6">
|
||||
|
@@ -6,22 +6,23 @@ interface Option {
|
||||
display: string | JSX.Element;
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
export function FilterDropdown({
|
||||
options,
|
||||
selected,
|
||||
handleSelect,
|
||||
icon,
|
||||
defaultDisplay,
|
||||
width = "w-64",
|
||||
}: {
|
||||
options: Option[];
|
||||
selected: string[];
|
||||
handleSelect: (option: Option) => void;
|
||||
icon: JSX.Element;
|
||||
defaultDisplay: string | JSX.Element;
|
||||
width?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="w-64">
|
||||
<div>
|
||||
<CustomDropdown
|
||||
dropdown={
|
||||
<div
|
||||
@@ -32,7 +33,7 @@ export function FilterDropdown({
|
||||
bg-background
|
||||
flex
|
||||
flex-col
|
||||
w-64
|
||||
${width}
|
||||
max-h-96
|
||||
overflow-y-auto
|
||||
overscroll-contain`}
|
||||
@@ -76,7 +77,7 @@ export function FilterDropdown({
|
||||
<div
|
||||
className={`
|
||||
flex
|
||||
w-64
|
||||
${width}
|
||||
text-sm
|
||||
px-3
|
||||
py-1.5
|
||||
|
@@ -3,7 +3,14 @@ import { DocumentSet, Tag, ValidSources } from "@/lib/types";
|
||||
import { SourceMetadata } from "@/lib/search/interfaces";
|
||||
import { InfoIcon, defaultTailwindCSS } from "../../icons/icons";
|
||||
import { HoverPopup } from "../../HoverPopup";
|
||||
import { FiBook, FiBookmark, FiFilter, FiMap, FiX } from "react-icons/fi";
|
||||
import {
|
||||
FiBook,
|
||||
FiBookmark,
|
||||
FiFilter,
|
||||
FiMap,
|
||||
FiTag,
|
||||
FiX,
|
||||
} from "react-icons/fi";
|
||||
import { DateRangeSelector } from "../DateRangeSelector";
|
||||
import { DateRangePickerValue } from "@tremor/react";
|
||||
import { FilterDropdown } from "./FilterDropdown";
|
||||
@@ -72,9 +79,9 @@ export function SourceSelector({
|
||||
<div
|
||||
className={`hidden ${
|
||||
showDocSidebar ? "4xl:block" : "!block"
|
||||
} duration-1000 ease-out transition-all transform origin-top-right`}
|
||||
} duration-1000 flex ease-out transition-all transform origin-top-right`}
|
||||
>
|
||||
<div className="flex mb-4 pb-2 border-b border-border text-emphasis">
|
||||
<div className=" mb-4 pb-2 border-b border-border text-emphasis">
|
||||
<h2 className="font-bold my-auto">Filters</h2>
|
||||
<FiFilter className="my-auto ml-2" size="16" />
|
||||
</div>
|
||||
@@ -324,3 +331,184 @@ export function HorizontalFilters({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function HorizontalSourceSelector({
|
||||
timeRange,
|
||||
setTimeRange,
|
||||
selectedSources,
|
||||
setSelectedSources,
|
||||
selectedDocumentSets,
|
||||
setSelectedDocumentSets,
|
||||
selectedTags,
|
||||
setSelectedTags,
|
||||
availableDocumentSets,
|
||||
existingSources,
|
||||
availableTags,
|
||||
}: SourceSelectorProps) {
|
||||
const handleSourceSelect = (source: SourceMetadata) => {
|
||||
setSelectedSources((prev: SourceMetadata[]) => {
|
||||
if (prev.map((s) => s.internalName).includes(source.internalName)) {
|
||||
return prev.filter((s) => s.internalName !== source.internalName);
|
||||
} else {
|
||||
return [...prev, source];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDocumentSetSelect = (documentSetName: string) => {
|
||||
setSelectedDocumentSets((prev: string[]) => {
|
||||
if (prev.includes(documentSetName)) {
|
||||
return prev.filter((s) => s !== documentSetName);
|
||||
} else {
|
||||
return [...prev, documentSetName];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleTagSelect = (tag: Tag) => {
|
||||
setSelectedTags((prev: Tag[]) => {
|
||||
if (
|
||||
prev.some(
|
||||
(t) => t.tag_key === tag.tag_key && t.tag_value === tag.tag_value
|
||||
)
|
||||
) {
|
||||
return prev.filter(
|
||||
(t) => !(t.tag_key === tag.tag_key && t.tag_value === tag.tag_value)
|
||||
);
|
||||
} else {
|
||||
return [...prev, tag];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex space-x-2">
|
||||
<div className="w-24">
|
||||
<DateRangeSelector
|
||||
isHoritontal
|
||||
value={timeRange}
|
||||
onValueChange={setTimeRange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{existingSources.length > 0 && (
|
||||
<FilterDropdown
|
||||
options={listSourceMetadata()
|
||||
.filter((source) => existingSources.includes(source.internalName))
|
||||
.map((source) => ({
|
||||
key: source.internalName,
|
||||
display: (
|
||||
<>
|
||||
<SourceIcon
|
||||
sourceType={source.internalName}
|
||||
iconSize={16}
|
||||
/>
|
||||
<span className="ml-2 text-sm">{source.displayName}</span>
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
selected={selectedSources.map((source) => source.internalName)}
|
||||
handleSelect={(option) =>
|
||||
handleSourceSelect(
|
||||
listSourceMetadata().find((s) => s.internalName === option.key)!
|
||||
)
|
||||
}
|
||||
icon={<FiMap size={16} />}
|
||||
defaultDisplay="Sources"
|
||||
width="w-fit max-w-24 ellipsis truncate"
|
||||
/>
|
||||
)}
|
||||
|
||||
{availableDocumentSets.length > 0 && (
|
||||
<FilterDropdown
|
||||
options={availableDocumentSets.map((documentSet) => ({
|
||||
key: documentSet.name,
|
||||
display: (
|
||||
<>
|
||||
<FiBookmark />
|
||||
<span className="ml-2 text-sm">{documentSet.name}</span>
|
||||
</>
|
||||
),
|
||||
}))}
|
||||
selected={selectedDocumentSets}
|
||||
handleSelect={(option) => handleDocumentSetSelect(option.key)}
|
||||
icon={<FiBook size={16} />}
|
||||
defaultDisplay="Sets"
|
||||
width="w-fit max-w-24 ellipsis"
|
||||
/>
|
||||
)}
|
||||
|
||||
{availableTags.length > 0 && (
|
||||
<FilterDropdown
|
||||
options={availableTags.map((tag) => ({
|
||||
key: `${tag.tag_key}=${tag.tag_value}`,
|
||||
display: (
|
||||
<span className="text-sm">
|
||||
{tag.tag_key}
|
||||
<b>=</b>
|
||||
{tag.tag_value}
|
||||
</span>
|
||||
),
|
||||
}))}
|
||||
selected={selectedTags.map(
|
||||
(tag) => `${tag.tag_key}=${tag.tag_value}`
|
||||
)}
|
||||
handleSelect={(option) => {
|
||||
const [tag_key, tag_value] = option.key.split("=");
|
||||
const selectedTag = availableTags.find(
|
||||
(tag) => tag.tag_key === tag_key && tag.tag_value === tag_value
|
||||
);
|
||||
if (selectedTag) {
|
||||
handleTagSelect(selectedTag);
|
||||
}
|
||||
}}
|
||||
icon={<FiTag size={16} />}
|
||||
defaultDisplay="Tags"
|
||||
width="w-fit max-w-24 ellipsis"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* <div className="flex flex-wrap gap-2">
|
||||
{timeRange && timeRange.selectValue && (
|
||||
<SelectedBubble onClick={() => setTimeRange(null)}>
|
||||
<div className="text-sm flex">{timeRange.selectValue}</div>
|
||||
</SelectedBubble>
|
||||
)}
|
||||
{selectedSources.map((source) => (
|
||||
<SelectedBubble
|
||||
key={source.internalName}
|
||||
onClick={() => handleSourceSelect(source)}
|
||||
>
|
||||
<>
|
||||
<SourceIcon sourceType={source.internalName} iconSize={16} />
|
||||
<span className="ml-2 text-sm">{source.displayName}</span>
|
||||
</>
|
||||
</SelectedBubble>
|
||||
))}
|
||||
{selectedDocumentSets.map((documentSetName) => (
|
||||
<SelectedBubble
|
||||
key={documentSetName}
|
||||
onClick={() => handleDocumentSetSelect(documentSetName)}
|
||||
>
|
||||
<>
|
||||
<FiBookmark />
|
||||
<span className="ml-2 text-sm">{documentSetName}</span>
|
||||
</>
|
||||
</SelectedBubble>
|
||||
))}
|
||||
{selectedTags.map((tag) => (
|
||||
<SelectedBubble
|
||||
key={`${tag.tag_key}=${tag.tag_value}`}
|
||||
onClick={() => handleTagSelect(tag)}
|
||||
>
|
||||
<span className="text-sm">
|
||||
{tag.tag_key}<b>=</b>{tag.tag_value}
|
||||
</span>
|
||||
</SelectedBubble>
|
||||
))}
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -26,31 +26,28 @@ interface AnswerSectionProps {
|
||||
answer: string | null;
|
||||
quotes: Quote[] | null;
|
||||
error: string | null;
|
||||
nonAnswerableReason: string | null;
|
||||
isFetching: boolean;
|
||||
}
|
||||
|
||||
export const AnswerSection = (props: AnswerSectionProps) => {
|
||||
let status = "in-progress" as StatusOptions;
|
||||
let header = <>Building answer...</>;
|
||||
let header = <></>;
|
||||
let body = null;
|
||||
|
||||
// finished answer
|
||||
if (props.quotes !== null || !props.isFetching) {
|
||||
status = "success";
|
||||
header = <>AI answer</>;
|
||||
if (props.answer) {
|
||||
body = (
|
||||
<ReactMarkdown
|
||||
className="prose text-sm max-w-full"
|
||||
remarkPlugins={[remarkGfm]}
|
||||
>
|
||||
{replaceNewlines(props.answer)}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
} else {
|
||||
body = <div>Information not found</div>;
|
||||
}
|
||||
header = <></>;
|
||||
|
||||
body = (
|
||||
<ReactMarkdown
|
||||
className="prose text-sm max-w-full"
|
||||
remarkPlugins={[remarkGfm]}
|
||||
>
|
||||
{replaceNewlines(props.answer || "")}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
|
||||
// error while building answer (NOTE: if error occurs during quote generation
|
||||
// the above if statement will hit and the error will not be displayed)
|
||||
} else if (props.error) {
|
||||
@@ -64,7 +61,7 @@ export const AnswerSection = (props: AnswerSectionProps) => {
|
||||
// answer is streaming
|
||||
} else if (props.answer) {
|
||||
status = "success";
|
||||
header = <>AI answer</>;
|
||||
header = <></>;
|
||||
body = (
|
||||
<ReactMarkdown
|
||||
className="prose text-sm max-w-full"
|
||||
@@ -74,10 +71,6 @@ export const AnswerSection = (props: AnswerSectionProps) => {
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
if (props.nonAnswerableReason) {
|
||||
status = "warning";
|
||||
header = <>Building best effort AI answer...</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponseSection
|
||||
@@ -87,20 +80,7 @@ export const AnswerSection = (props: AnswerSectionProps) => {
|
||||
<div className="ml-2 text-strong">{header}</div>
|
||||
</div>
|
||||
}
|
||||
body={
|
||||
<div className="">
|
||||
{body}
|
||||
{props.nonAnswerableReason && !props.isFetching && (
|
||||
<div className="mt-4 text-sm">
|
||||
<b className="font-medium">Warning:</b> the AI did not think this
|
||||
question was answerable.{" "}
|
||||
<div className="italic mt-1 ml-2">
|
||||
{props.nonAnswerableReason}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
body={<div className="">{body}</div>}
|
||||
desiredOpenStatus={true}
|
||||
isNotControllable={true}
|
||||
/>
|
||||
|
@@ -65,7 +65,6 @@ const QuoteDisplay = ({ quoteInfo }: { quoteInfo: Quote }) => {
|
||||
|
||||
interface QuotesSectionProps {
|
||||
quotes: Quote[] | null;
|
||||
isAnswerable: boolean | null;
|
||||
isFetching: boolean;
|
||||
}
|
||||
|
||||
@@ -110,11 +109,7 @@ export const QuotesSection = (props: QuotesSectionProps) => {
|
||||
let status: StatusOptions = "in-progress";
|
||||
if (!props.isFetching) {
|
||||
if (props.quotes && props.quotes.length > 0) {
|
||||
if (props.isAnswerable === false) {
|
||||
status = "warning";
|
||||
} else {
|
||||
status = "success";
|
||||
}
|
||||
status = "success";
|
||||
} else {
|
||||
status = "failed";
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
} from "@/components/icons/icons";
|
||||
import { useState } from "react";
|
||||
import { Grid } from "react-loader-spinner";
|
||||
import { searchState } from "../SearchSection";
|
||||
|
||||
export type StatusOptions = "in-progress" | "failed" | "warning" | "success";
|
||||
|
||||
@@ -31,26 +32,13 @@ export const ResponseSection = ({
|
||||
|
||||
let icon = null;
|
||||
if (status === "in-progress") {
|
||||
icon = (
|
||||
<div className="m-auto">
|
||||
<Grid
|
||||
height="12"
|
||||
width="12"
|
||||
color="#3b82f6"
|
||||
ariaLabel="grid-loading"
|
||||
radius="12.5"
|
||||
wrapperStyle={{}}
|
||||
wrapperClass=""
|
||||
visible={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
icon = <></>;
|
||||
}
|
||||
if (status === "failed") {
|
||||
icon = <AlertIcon size={16} className="text-red-500" />;
|
||||
}
|
||||
if (status === "success") {
|
||||
icon = <CheckmarkIcon size={16} className="text-green-600" />;
|
||||
icon = <></>;
|
||||
}
|
||||
if (status === "warning") {
|
||||
icon = <TriangleAlertIcon size={16} className="text-yellow-600" />;
|
||||
|
@@ -52,5 +52,5 @@ export const CUSTOM_ANALYTICS_ENABLED = process.env.CUSTOM_ANALYTICS_SECRET_KEY
|
||||
? true
|
||||
: false;
|
||||
|
||||
export const DISABLE_AGENTIC_SEARCH =
|
||||
process.env.DISABLE_AGENTIC_SEARCH?.toLowerCase() === "true";
|
||||
export const DISABLE_LLM_DOC_RELEVANCE =
|
||||
process.env.DISABLE_LLM_DOC_RELEVANCE?.toLowerCase() === "true";
|
||||
|
@@ -158,7 +158,6 @@ export interface SearchRequestOverrides {
|
||||
}
|
||||
|
||||
export interface ValidQuestionResponse {
|
||||
answerable: boolean | null;
|
||||
reasoning: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
@@ -61,8 +61,7 @@ export const searchRequestStreamed = async ({
|
||||
filters: filters,
|
||||
enable_auto_detect_filters: false,
|
||||
},
|
||||
llm_doc_eval: true,
|
||||
skip_gen_ai_answer_generation: true,
|
||||
evaluation_type: agentic ? "agentic" : "basic",
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
@@ -1,66 +0,0 @@
|
||||
import {
|
||||
AnswerPiecePacket,
|
||||
ErrorMessagePacket,
|
||||
ValidQuestionResponse,
|
||||
} from "./interfaces";
|
||||
import { processRawChunkString } from "./streamingUtils";
|
||||
|
||||
export interface QuestionValidationArgs {
|
||||
query: string;
|
||||
update: (update: Partial<ValidQuestionResponse>) => void;
|
||||
}
|
||||
|
||||
export const questionValidationStreamed = async <T>({
|
||||
query,
|
||||
update,
|
||||
}: QuestionValidationArgs) => {
|
||||
const response = await fetch("/api/query/stream-query-validation", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const reader = response.body?.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
|
||||
let reasoning = "";
|
||||
let previousPartialChunk: string | null = null;
|
||||
while (true) {
|
||||
const rawChunk = await reader?.read();
|
||||
if (!rawChunk) {
|
||||
throw new Error("Unable to process chunk");
|
||||
}
|
||||
const { done, value } = rawChunk;
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
const [completedChunks, partialChunk] = processRawChunkString<
|
||||
AnswerPiecePacket | ValidQuestionResponse | ErrorMessagePacket
|
||||
>(decoder.decode(value, { stream: true }), previousPartialChunk);
|
||||
if (!completedChunks.length && !partialChunk) {
|
||||
break;
|
||||
}
|
||||
previousPartialChunk = partialChunk as string | null;
|
||||
|
||||
completedChunks.forEach((chunk) => {
|
||||
if (Object.hasOwn(chunk, "answer_piece")) {
|
||||
reasoning += (chunk as AnswerPiecePacket).answer_piece;
|
||||
update({
|
||||
reasoning,
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.hasOwn(chunk, "answerable")) {
|
||||
update({ answerable: (chunk as ValidQuestionResponse).answerable });
|
||||
}
|
||||
|
||||
if (Object.hasOwn(chunk, "error")) {
|
||||
update({ error: (chunk as ErrorMessagePacket).error });
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user