diff --git a/web/src/components/SearchResultsDisplay.tsx b/web/src/components/SearchResultsDisplay.tsx
deleted file mode 100644
index e53ac6448959..000000000000
--- a/web/src/components/SearchResultsDisplay.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import React from "react";
-import { Globe, SlackLogo, GoogleDriveLogo } from "@phosphor-icons/react";
-import "tailwindcss/tailwind.css";
-import { Quote, SearchResponse } from "./types";
-import { ThinkingAnimation } from "./Thinking";
-
-interface SearchResultsDisplayProps {
- data: SearchResponse | undefined;
- isFetching: boolean;
-}
-
-const ICON_SIZE = "20";
-const ICON_STYLE = "text-blue-600 my-auto mr-1 flex flex-shrink-0";
-
-const getSourceIcon = (sourceType: string) => {
- switch (sourceType) {
- case "web":
- return
;
- case "slack":
- return
;
- case "google_drive":
- return
;
- default:
- return null;
- }
-};
-
-export const SearchResultsDisplay: React.FC
= ({
- data,
- isFetching,
-}) => {
- if (isFetching) {
- return ;
- }
-
- if (!data) {
- return null;
- }
-
- const { answer, quotes } = data;
- if (!answer || !quotes) {
- return Unable to find an answer
;
- }
-
- const dedupedQuotes: Quote[] = [];
- const seen = new Set();
- Object.values(quotes).forEach((quote) => {
- if (!seen.has(quote.document_id)) {
- dedupedQuotes.push(quote);
- seen.add(quote.document_id);
- }
- });
-
- return (
- <>
-
-
AI Answer
-
{answer}
-
-
Sources
-
-
-
-
- Results
-
- {dedupedQuotes.map((quoteInfo) => (
-
- ))}
-
- >
- );
-};
diff --git a/web/src/components/SearchBar.tsx b/web/src/components/search/SearchBar.tsx
similarity index 53%
rename from web/src/components/SearchBar.tsx
rename to web/src/components/search/SearchBar.tsx
index 03a00e1ed624..6516048c1bfa 100644
--- a/web/src/components/SearchBar.tsx
+++ b/web/src/components/search/SearchBar.tsx
@@ -1,63 +1,20 @@
-"use client";
-
import React, { useState, KeyboardEvent, ChangeEvent } from "react";
import { MagnifyingGlass } from "@phosphor-icons/react";
-import "tailwindcss/tailwind.css";
-import { SearchResultsDisplay } from "./SearchResultsDisplay";
-import { SearchResponse } from "./types";
-
-const searchRequest = async (query: string): Promise => {
- const url = new URL("/api/direct-qa", window.location.origin);
- const params = new URLSearchParams({
- query,
- collection: "semantic_search",
- }).toString();
- url.search = params;
-
- const response = await fetch(url);
- return response.json();
-};
-
-export const SearchSection: React.FC<{}> = () => {
- const [answer, setAnswer] = useState();
- const [isFetching, setIsFetching] = useState(false);
-
- return (
- <>
- {
- setIsFetching(true);
- searchRequest(query).then((response) => {
- setIsFetching(false);
- setAnswer(response);
- });
- }}
- />
-
-
-
- >
- );
-};
interface SearchBarProps {
onSearch: (searchTerm: string) => void;
}
-const SearchBar: React.FC = ({ onSearch }) => {
+export const SearchBar: React.FC = ({ onSearch }) => {
const [searchTerm, setSearchTerm] = useState("");
const handleChange = (event: ChangeEvent) => {
const target = event.target;
setSearchTerm(target.value);
- // Reset the textarea height
+ // Resize the textarea to fit the content
target.style.height = "24px";
-
- // Calculate the new height based on scrollHeight
const newHeight = target.scrollHeight;
-
- // Apply the new height
target.style.height = `${newHeight}px`;
};
diff --git a/web/src/components/search/SearchResultsDisplay.tsx b/web/src/components/search/SearchResultsDisplay.tsx
new file mode 100644
index 000000000000..7fb98161627b
--- /dev/null
+++ b/web/src/components/search/SearchResultsDisplay.tsx
@@ -0,0 +1,115 @@
+import React from "react";
+import { Globe, SlackLogo, GoogleDriveLogo } from "@phosphor-icons/react";
+import "tailwindcss/tailwind.css";
+import { Quote, Document } from "./types";
+import { ThinkingAnimation } from "../Thinking";
+
+interface SearchResultsDisplayProps {
+ answer: string | null;
+ quotes: Record | null;
+ documents: Document[] | null;
+ isFetching: boolean;
+}
+
+const ICON_SIZE = "20";
+const ICON_STYLE = "text-blue-600 my-auto mr-1 flex flex-shrink-0";
+
+const getSourceIcon = (sourceType: string) => {
+ switch (sourceType) {
+ case "web":
+ return ;
+ case "slack":
+ return ;
+ case "google_drive":
+ return ;
+ default:
+ return null;
+ }
+};
+
+export const SearchResultsDisplay: React.FC = ({
+ answer,
+ quotes,
+ documents,
+ isFetching,
+}) => {
+ if (!answer) {
+ if (isFetching) {
+ return ;
+ }
+ return null;
+ }
+
+ if (answer === null) {
+ return Unable to find an answer
;
+ }
+
+ const dedupedQuotes: Quote[] = [];
+ const seen = new Set();
+ if (quotes) {
+ Object.values(quotes).forEach((quote) => {
+ if (!seen.has(quote.document_id)) {
+ dedupedQuotes.push(quote);
+ seen.add(quote.document_id);
+ }
+ });
+ }
+
+ return (
+ <>
+
+
AI Answer
+
{answer}
+
+ {dedupedQuotes.length > 0 && (
+ <>
+
Sources
+
+ >
+ )}
+
+ {/* Only display docs once we're done fetching to avoid distracting from the AI answer*/}
+ {!isFetching && documents && documents.length > 0 && (
+
+
+ Results
+
+ {documents.slice(0, 5).map((doc) => (
+
+ ))}
+
+ )}
+ >
+ );
+};
diff --git a/web/src/components/search/SearchSection.tsx b/web/src/components/search/SearchSection.tsx
new file mode 100644
index 000000000000..a0a2dc932e01
--- /dev/null
+++ b/web/src/components/search/SearchSection.tsx
@@ -0,0 +1,93 @@
+"use client";
+
+import { useState } from "react";
+import { SearchBar } from "./SearchBar";
+import { SearchResultsDisplay } from "./SearchResultsDisplay";
+import { Quote, Document } from "./types";
+
+const searchRequestStreamed = async (
+ query: string,
+ updateCurrentAnswer: (val: string) => void,
+ updateQuotes: (quotes: Record) => void,
+ updateDocs: (docs: Document[]) => void
+) => {
+ const url = new URL("/api/stream-direct-qa", window.location.origin);
+ const params = new URLSearchParams({
+ query,
+ collection: "semantic_search",
+ }).toString();
+ url.search = params;
+
+ let answer = "";
+ try {
+ const response = await fetch(url);
+ const reader = response.body?.getReader();
+ const decoder = new TextDecoder("utf-8");
+
+ while (true) {
+ const rawChunk = await reader?.read();
+ if (!rawChunk) {
+ throw new Error("Unable to process chunk");
+ }
+ const { done, value } = rawChunk;
+ if (done) {
+ break;
+ }
+
+ // Process each chunk as it arrives
+ const chunk = decoder.decode(value, { stream: true });
+ if (!chunk) {
+ break;
+ }
+ const chunkJson = JSON.parse(chunk);
+ const answerChunk = chunkJson.answer_data;
+ if (answerChunk) {
+ answer += answerChunk;
+ updateCurrentAnswer(answer);
+ } else {
+ const docs = chunkJson.top_documents as any[];
+ if (docs) {
+ updateDocs(docs.map((doc) => JSON.parse(doc)));
+ } else {
+ updateQuotes(chunkJson);
+ }
+ }
+ }
+ } catch (err) {
+ console.error("Fetch error:", err);
+ }
+ return answer;
+};
+
+export const SearchSection: React.FC<{}> = () => {
+ const [answer, setAnswer] = useState("");
+ const [quotes, setQuotes] = useState | null>(null);
+ const [documents, setDocuments] = useState(null);
+ const [isFetching, setIsFetching] = useState(false);
+
+ return (
+ <>
+ {
+ setIsFetching(true);
+ setAnswer("");
+ setQuotes(null);
+ setDocuments(null);
+ searchRequestStreamed(query, setAnswer, setQuotes, setDocuments).then(
+ () => {
+ setIsFetching(false);
+ }
+ );
+ }}
+ />
+
+
+
+ >
+ );
+};
diff --git a/web/src/components/types.tsx b/web/src/components/search/types.tsx
similarity index 62%
rename from web/src/components/types.tsx
rename to web/src/components/search/types.tsx
index 6e6a7cacf6e4..928d006eef54 100644
--- a/web/src/components/types.tsx
+++ b/web/src/components/search/types.tsx
@@ -6,6 +6,14 @@ export interface Quote {
semantic_identifier: string | null;
}
+export interface Document {
+ document_id: string;
+ link: string;
+ source_type: string;
+ blurb: string;
+ semantic_name: string | null;
+}
+
export interface SearchResponse {
answer: string;
quotes: Record;