diff --git a/web/src/app/admin/bot/SlackBotConfigCreationForm.tsx b/web/src/app/admin/bot/SlackBotConfigCreationForm.tsx index 8ae9bc1be9a6..916fca142a5a 100644 --- a/web/src/app/admin/bot/SlackBotConfigCreationForm.tsx +++ b/web/src/app/admin/bot/SlackBotConfigCreationForm.tsx @@ -12,7 +12,7 @@ import { channel } from "diagnostics_channel"; interface SetCreationPopupProps { onClose: () => void; setPopup: (popupSpec: PopupSpec | null) => void; - documentSets: DocumentSet[]; + documentSets: DocumentSet[]; existingSlackBotConfig?: SlackBotConfig; } diff --git a/web/src/app/admin/bot/page.tsx b/web/src/app/admin/bot/page.tsx index 58f37482dc36..e84e1c475b54 100644 --- a/web/src/app/admin/bot/page.tsx +++ b/web/src/app/admin/bot/page.tsx @@ -29,7 +29,7 @@ const EditRow = ({ }: { existingSlackBotConfig: SlackBotConfig; setPopup: (popupSpec: PopupSpec | null) => void; - documentSets: DocumentSet[]; + documentSets: DocumentSet[]; refreshSlackBotConfigs: () => void; }) => { const [isEditPopupOpen, setEditPopupOpen] = useState(false); @@ -58,7 +58,7 @@ const EditRow = ({ interface DocumentFeedbackTableProps { slackBotConfigs: SlackBotConfig[]; - documentSets: DocumentSet[]; + documentSets: DocumentSet[]; refresh: () => void; setPopup: (popupSpec: PopupSpec | null) => void; } diff --git a/web/src/app/admin/documents/sets/DocumentSetCreationForm.tsx b/web/src/app/admin/documents/sets/DocumentSetCreationForm.tsx index 2a01a2b1c630..615e44944cab 100644 --- a/web/src/app/admin/documents/sets/DocumentSetCreationForm.tsx +++ b/web/src/app/admin/documents/sets/DocumentSetCreationForm.tsx @@ -10,7 +10,7 @@ interface SetCreationPopupProps { ccPairs: ConnectorIndexingStatus[]; onClose: () => void; setPopup: (popupSpec: PopupSpec | null) => void; - existingDocumentSet?: DocumentSet; + existingDocumentSet?: DocumentSet; } export const DocumentSetCreationForm = ({ diff --git a/web/src/app/admin/documents/sets/hooks.tsx b/web/src/app/admin/documents/sets/hooks.tsx index 2a510438887e..179e36385dd7 100644 --- a/web/src/app/admin/documents/sets/hooks.tsx +++ b/web/src/app/admin/documents/sets/hooks.tsx @@ -4,10 +4,7 @@ import useSWR, { mutate } from "swr"; export const useDocumentSets = () => { const url = "/api/manage/document-set"; - const swrResponse = useSWR[]>( - url, - errorHandlingFetcher - ); + const swrResponse = useSWR(url, errorHandlingFetcher); return { ...swrResponse, diff --git a/web/src/app/admin/documents/sets/page.tsx b/web/src/app/admin/documents/sets/page.tsx index 7b6afb2c8dae..2a6a63f60d33 100644 --- a/web/src/app/admin/documents/sets/page.tsx +++ b/web/src/app/admin/documents/sets/page.tsx @@ -27,7 +27,7 @@ const EditRow = ({ setPopup, refreshDocumentSets, }: { - documentSet: DocumentSet; + documentSet: DocumentSet; ccPairs: ConnectorIndexingStatus[]; setPopup: (popupSpec: PopupSpec | null) => void; refreshDocumentSets: () => void; @@ -81,7 +81,7 @@ const EditRow = ({ }; interface DocumentFeedbackTableProps { - documentSets: DocumentSet[]; + documentSets: DocumentSet[]; ccPairs: ConnectorIndexingStatus[]; refresh: () => void; setPopup: (popupSpec: PopupSpec | null) => void; diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 86d252549153..583684d723fd 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -6,7 +6,7 @@ import { DISABLE_AUTH } from "@/lib/constants"; import { HealthCheckBanner } from "@/components/health/healthcheck"; import { ApiKeyModal } from "@/components/openai/ApiKeyModal"; import { buildUrl } from "@/lib/utilsSS"; -import { Connector, User } from "@/lib/types"; +import { Connector, DocumentSet, User } from "@/lib/types"; import { cookies } from "next/headers"; import { SearchType } from "@/lib/search/interfaces"; @@ -19,6 +19,12 @@ export default async function Home() { cookie: processCookies(cookies()), }, }), + fetch(buildUrl("/manage/document-set"), { + next: { revalidate: 0 }, + headers: { + cookie: processCookies(cookies()), + }, + }), ]; // catch cases where the backend is completely unreachable here @@ -32,6 +38,7 @@ export default async function Home() { } const user = results[0] as User | null; const connectorsResponse = results[1] as Response | null; + const documentSetsResponse = results[2] as Response | null; if (!DISABLE_AUTH && !user) { return redirect("/auth/login"); @@ -44,6 +51,15 @@ export default async function Home() { console.log(`Failed to fetch connectors - ${connectorsResponse?.status}`); } + let documentSets: DocumentSet[] = []; + if (documentSetsResponse?.ok) { + documentSets = await documentSetsResponse.json(); + } else { + console.log( + `Failed to fetch document sets - ${documentSetsResponse?.status}` + ); + } + // needs to be done in a non-client side component due to nextjs const storedSearchType = cookies().get("searchType")?.value as | string @@ -65,6 +81,7 @@ export default async function Home() {
diff --git a/web/src/components/HoverPopup.tsx b/web/src/components/HoverPopup.tsx new file mode 100644 index 000000000000..c78d762f605f --- /dev/null +++ b/web/src/components/HoverPopup.tsx @@ -0,0 +1,35 @@ +import { useState } from "react"; + +interface HoverPopupProps { + mainContent: string | JSX.Element; + popupContent: string | JSX.Element; + classNameModifications?: string; +} + +export const HoverPopup = ({ + mainContent, + popupContent, + classNameModifications, +}: HoverPopupProps) => { + const [hovered, setHovered] = useState(false); + + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + {hovered && ( +
+ {popupContent} +
+ )} + {mainContent} +
+ ); +}; diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index 572577b298f5..4b5d2344fcfa 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -4,7 +4,6 @@ import { Notebook, Key, Trash, - Info, XSquare, LinkBreak, Link, @@ -33,6 +32,7 @@ import { FiCopy, FiBookmark, FiCpu, + FiInfo, } from "react-icons/fi"; import { SiBookstack } from "react-icons/si"; import Image from "next/image"; @@ -47,7 +47,7 @@ interface IconProps { className?: string; } -const defaultTailwindCSS = "my-auto flex flex-shrink-0 text-blue-400"; +export const defaultTailwindCSS = "my-auto flex flex-shrink-0 text-blue-400"; export const PlugIcon = ({ size = 16, @@ -123,7 +123,7 @@ export const InfoIcon = ({ size = 16, className = defaultTailwindCSS, }: IconProps) => { - return ; + return ; }; export const QuestionIcon = ({ diff --git a/web/src/components/search/Filters.tsx b/web/src/components/search/Filters.tsx index 485548b7b159..14e4e9ad6b12 100644 --- a/web/src/components/search/Filters.tsx +++ b/web/src/components/search/Filters.tsx @@ -1,8 +1,16 @@ import React from "react"; import { getSourceIcon } from "../source"; import { Funnel } from "@phosphor-icons/react"; -import { ValidSources } from "@/lib/types"; +import { DocumentSet, ValidSources } from "@/lib/types"; import { Source } from "@/lib/search/interfaces"; +import { + BookmarkIcon, + InfoIcon, + NotebookIcon, + defaultTailwindCSS, +} from "../icons/icons"; +import { HoverPopup } from "../HoverPopup"; +import { FiFilter } from "react-icons/fi"; const sources: Source[] = [ { displayName: "Google Drive", internalName: "google_drive" }, @@ -24,12 +32,18 @@ const sources: Source[] = [ interface SourceSelectorProps { selectedSources: Source[]; setSelectedSources: React.Dispatch>; + selectedDocumentSets: string[]; + setSelectedDocumentSets: React.Dispatch>; + availableDocumentSets: DocumentSet[]; existingSources: ValidSources[]; } export function SourceSelector({ selectedSources, setSelectedSources, + selectedDocumentSets, + setSelectedDocumentSets, + availableDocumentSets, existingSources, }: SourceSelectorProps) { const handleSelect = (source: Source) => { @@ -42,34 +56,97 @@ export function SourceSelector({ }); }; + const handleDocumentSetSelect = (documentSetName: string) => { + setSelectedDocumentSets((prev: string[]) => { + if (prev.includes(documentSetName)) { + return prev.filter((s) => s !== documentSetName); + } else { + return [...prev, documentSetName]; + } + }); + }; + return ( -
-
+
+

Filters

- -
-
- {sources - .filter((source) => existingSources.includes(source.internalName)) - .map((source) => ( -
handleSelect(source)} - > - {getSourceIcon(source.internalName, 16)} - - {source.displayName} - -
- ))} +
+ + {existingSources.length > 0 && ( + <> +
Sources
+
+ {sources + .filter((source) => existingSources.includes(source.internalName)) + .map((source) => ( +
handleSelect(source)} + > + {getSourceIcon(source.internalName, 16)} + + {source.displayName} + +
+ ))} +
+ + )} + + {availableDocumentSets.length > 0 && ( + <> +
+
Knowledge Sets
+
+
+ {availableDocumentSets.map((documentSet) => ( +
+
handleDocumentSetSelect(documentSet.name)} + > + + +
+ } + popupContent={ +
+
+ Description +
+
+ {documentSet.description} +
+
+ } + classNameModifications="-ml-2" + /> + + {documentSet.name} + +
+
+ ))} +
+ + )}
); } diff --git a/web/src/components/search/SearchSection.tsx b/web/src/components/search/SearchSection.tsx index f390d8e0e662..d0c19e5399e7 100644 --- a/web/src/components/search/SearchSection.tsx +++ b/web/src/components/search/SearchSection.tsx @@ -4,7 +4,7 @@ import { useRef, useState } from "react"; import { SearchBar } from "./SearchBar"; import { SearchResultsDisplay } from "./SearchResultsDisplay"; import { SourceSelector } from "./Filters"; -import { Connector } from "@/lib/types"; +import { Connector, DocumentSet } from "@/lib/types"; import { SearchTypeSelector } from "./SearchTypeSelector"; import { DanswerDocument, @@ -38,13 +38,16 @@ const VALID_QUESTION_RESPONSE_DEFAULT: ValidQuestionResponse = { interface SearchSectionProps { connectors: Connector[]; + documentSets: DocumentSet[]; defaultSearchType: SearchType; } export const SearchSection: React.FC = ({ connectors, + documentSets, defaultSearchType, }) => { + console.log(documentSets); // Search Bar const [query, setQuery] = useState(""); @@ -59,6 +62,9 @@ export const SearchSection: React.FC = ({ // Filters const [sources, setSources] = useState([]); + const [selectedDocumentSets, setSelectedDocumentSets] = useState( + [] + ); // Search Type const [selectedSearchType, setSelectedSearchType] = @@ -135,6 +141,7 @@ export const SearchSection: React.FC = ({ const searchFnArgs = { query, sources, + documentSets: selectedDocumentSets, updateCurrentAnswer: cancellable({ cancellationToken: lastSearchCancellationToken.current, fn: updateCurrentAnswer, @@ -183,10 +190,13 @@ export const SearchSection: React.FC = ({ return (
- {connectors.length > 0 && ( + {(connectors.length > 0 || documentSets.length > 0) && ( connector.source)} /> )} diff --git a/web/src/lib/search/interfaces.ts b/web/src/lib/search/interfaces.ts index d813f5a65942..56327994f0ff 100644 --- a/web/src/lib/search/interfaces.ts +++ b/web/src/lib/search/interfaces.ts @@ -59,6 +59,7 @@ export interface SearchDefaultOverrides { export interface SearchRequestArgs { query: string; sources: Source[]; + documentSets: string[]; updateCurrentAnswer: (val: string) => void; updateQuotes: (quotes: Quote[]) => void; updateDocs: (documents: DanswerDocument[]) => void; diff --git a/web/src/lib/search/qa.ts b/web/src/lib/search/qa.ts index d7258ca05f24..c4b1b8b8189b 100644 --- a/web/src/lib/search/qa.ts +++ b/web/src/lib/search/qa.ts @@ -5,10 +5,12 @@ import { SearchRequestArgs, SearchType, } from "./interfaces"; +import { buildFilters } from "./utils"; export const searchRequest = async ({ query, sources, + documentSets, updateCurrentAnswer, updateQuotes, updateDocs, @@ -27,19 +29,16 @@ export const searchRequest = async ({ let quotes: Quote[] | null = null; let relevantDocuments: DanswerDocument[] | null = null; try { + const filters = buildFilters(sources, documentSets); const response = await fetch("/api/direct-qa", { method: "POST", body: JSON.stringify({ query, collection: "danswer_index", use_keyword: useKeyword, - ...(sources.length > 0 + ...(filters.length > 0 ? { - filters: [ - { - source_type: sources.map((source) => source.internalName), - }, - ], + filters, } : {}), offset: offset, diff --git a/web/src/lib/search/streamingQa.ts b/web/src/lib/search/streamingQa.ts index 8225c4dd639b..e975be7e477f 100644 --- a/web/src/lib/search/streamingQa.ts +++ b/web/src/lib/search/streamingQa.ts @@ -4,6 +4,7 @@ import { SearchRequestArgs, SearchType, } from "./interfaces"; +import { buildFilters } from "./utils"; const processSingleChunk = ( chunk: string, @@ -54,6 +55,7 @@ const processRawChunkString = ( export const searchRequestStreamed = async ({ query, sources, + documentSets, updateCurrentAnswer, updateQuotes, updateDocs, @@ -73,19 +75,16 @@ export const searchRequestStreamed = async ({ let quotes: Quote[] | null = null; let relevantDocuments: DanswerDocument[] | null = null; try { + const filters = buildFilters(sources, documentSets); const response = await fetch("/api/stream-direct-qa", { method: "POST", body: JSON.stringify({ query, collection: "danswer_index", use_keyword: useKeyword, - ...(sources.length > 0 + ...(filters.length > 0 ? { - filters: [ - { - source_type: sources.map((source) => source.internalName), - }, - ], + filters, } : {}), offset: offset, diff --git a/web/src/lib/search/utils.ts b/web/src/lib/search/utils.ts new file mode 100644 index 000000000000..d1d5e4ed6343 --- /dev/null +++ b/web/src/lib/search/utils.ts @@ -0,0 +1,16 @@ +import { Source } from "./interfaces"; + +export const buildFilters = (sources: Source[], documentSets: string[]) => { + const filters = []; + if (sources.length > 0) { + filters.push({ + source_type: sources.map((source) => source.internalName), + }); + } + if (documentSets.length > 0) { + filters.push({ + document_sets: documentSets, + }); + } + return filters; +}; diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 74c189b451ac..594f819d8c21 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -222,11 +222,11 @@ export interface CCPairDescriptor { credential: Credential; } -export interface DocumentSet { +export interface DocumentSet { id: number; name: string; description: string; - cc_pair_descriptors: CCPairDescriptor[]; + cc_pair_descriptors: CCPairDescriptor[]; is_up_to_date: boolean; } @@ -239,7 +239,7 @@ export interface ChannelConfig { export interface SlackBotConfig { id: number; - document_sets: DocumentSet[]; + document_sets: DocumentSet[]; channel_config: ChannelConfig; }