import { useState, useEffect, useRef } from "react"; import { getClassOverride, fetcher, twMerge } from "../utils"; import { MagicSearchProps, Message } from "../utils/types"; import { MAGIC_SEARCH_ID, HEADER_ID, TAB, MAGIC_SEARCH_CONTENT_ID, } from "../utils/constants"; import Home from "../screens/Home"; import Results from "../screens/Results"; import TabIcon from "./TabIcon"; import NavButton from "./NavButton"; import { useLogger } from "../context/LoggerProvider"; import ErrorBoundary from "./ErrorBoundary"; import { useTracker } from "../context/TrackerProvider"; import { useConfiguration } from "../context/ConfigurationProvider"; import SearchIcon from "../icons/SearchIcon"; // Other const BOT_ROLE = "assistant"; const USER_ROLE = "user"; let buffer = ""; let isCitation = false; let citationIndex = 0; const citationIndexMap: { [_key in string]: number } = {}; const processMessage = (msg: string) => { let result = ""; for (let char of msg) { if (char === "(") { isCitation = true; buffer += char; continue; } if (isCitation) { buffer += char; if (char === ")") { isCitation = false; const citation = buffer.slice(1, -1); const citationList = citation.split(";").map((c) => c.trim()); citationList.forEach((citation) => { if (!citationIndexMap[citation]) { citationIndexMap[citation] = citationIndex + 1; } result += `([${citationIndexMap[citation]}](${citation}))`; citationIndex++; }); buffer = ""; } } else { result += char; } } return result; }; const bodyClassesLeft = ["tb-ml-0", "md:tb-ml-72", "lg:tb-ml-96"]; const transitionClassesLeft = [ "tb-transition-[margin-left]", "tb-duration-500", ]; const bodyClassesRight = ["tb-mr-0", "md:tb-mr-72", "lg:tb-mr-96"]; const transitionClassesRight = [ "tb-transition-[margin-right]", "tb-duration-500", ]; const MagicSearch = ({ direction, shiftBody = true, publicKey, }: MagicSearchProps) => { const logger = useLogger(); const tracker = useTracker(); const configuration = useConfiguration(); const searchInputRef = useRef(null); const previewSearchRef = useRef(""); const [searchTerm, setSearchTerm] = useState(""); const [messages, setMessages] = useState([]); const [articles, setArticles] = useState<{ [_key in number]: any[] }>({}); const [showMagicSearch, setShowMagicSearch] = useState(false); const [prompts, setPrompts] = useState([]); const [page, setPage] = useState(0); const [isStreamActive, setIsStreamActive] = useState(false); const stopStream = useRef(false); const setStopStream = (stop: boolean) => (stopStream.current = stop); useEffect(() => { if (showMagicSearch && prompts && !prompts.length) { fetcher({ path: "/content/v1/search/questions", key: publicKey, body: { content: document.getElementsByTagName("body")[0]?.innerText, }, host: configuration.host, }).then((res: Response) => { if (!res.ok) { logger.error("Failed to fetch prompts", { status: res.status }); setPrompts(null); return; } res.json().then((data) => { if (!data || data?.length === 0) { logger.info("No prompts found"); setPrompts(null); return; } setPrompts(data as string[]); }); }); } }, [showMagicSearch, prompts]); useEffect(() => { if (showMagicSearch) { tracker.trackPageview(); } }, [showMagicSearch]); useEffect(() => { // Shift page body on open/close if (showMagicSearch && shiftBody) { document.body.classList.add( ...(direction === "left" ? [...bodyClassesLeft, ...transitionClassesLeft] : [...bodyClassesRight, ...transitionClassesRight]), ); } if (!showMagicSearch && shiftBody) { if (direction === "left") { document.body.classList.remove(...bodyClassesLeft); } if (direction === "right") { document.body.classList.remove(...bodyClassesRight); } } if (searchInputRef.current && showMagicSearch) { searchInputRef.current.focus(); } }, [showMagicSearch]); useEffect(() => { // If we just appended a user message, we should fetch the articles for that prompt if (messages[messages.length - 1]?.role === USER_ROLE) { const lastSearchValue = messages[messages.length - 1].content; // Track the new search tracker.trackEvent("search", { props: { searchValue: lastSearchValue, page }, }); setIsStreamActive(true); // Set the new page to the last page setPage(Math.ceil(messages.length / 2)); fetcher({ path: "/content/v1/search/articles", key: publicKey, body: { query: lastSearchValue, numberOfResults: 10, }, host: configuration.host, }).then((res: Response) => { if (!res.ok) { logger.error("Failed to fetch articles", { status: res.status }); return; } res.json().then((data) => { if (!data) { logger.info("No articles found", { query: lastSearchValue }); return; } setArticles((prevState) => ({ ...prevState, [messages.length - 1]: data, })); previewSearchRef.current = lastSearchValue; // @ts-expect-error searchInputRef?.current.value = ""; }); }); } }, [messages]); useEffect(() => { const lastMessage = messages[messages.length - 1]; const mostRecentArticles = articles[messages.length - 1]; if ( lastMessage?.role === USER_ROLE && mostRecentArticles && mostRecentArticles.length > 0 ) { const fetchAnswer = async () => { const stream = await fetcher({ path: "/content/v1/search/summary", key: publicKey, body: { articles: mostRecentArticles, messages, query: lastMessage.content, }, host: configuration.host, }); const reader = stream?.body?.getReader(); const decoder = new TextDecoder(); if (!reader) return; while (true) { const { done, value } = await reader.read(); if (done) { setIsStreamActive(false); break; } // Stop the stream if the user presses the stop button if (stopStream.current) { setStopStream(false); setIsStreamActive(false); // If the stream hasnt started to process the message we need to add an empty bot message to keep the order of the messages user -> bot -> user -> bot setMessages((prevMessages) => { if (prevMessages[prevMessages.length - 1]?.role !== BOT_ROLE) { return [ ...prevMessages, { role: BOT_ROLE, content: "", }, ]; } return prevMessages; }); reader.cancel(); break; } setMessages((prevMessages) => { if (prevMessages[prevMessages.length - 1]?.role === BOT_ROLE) { return [ ...prevMessages.slice(0, -1), { role: BOT_ROLE, content: prevMessages[prevMessages.length - 1].content + processMessage(decoder.decode(value, { stream: true })), }, ]; } else { return [ ...prevMessages, { role: BOT_ROLE, content: processMessage( decoder.decode(value, { stream: true }), ), }, ]; } }); } }; fetchAnswer(); } }, [articles]); const submitSearch = (value: string) => { setMessages((prevState) => [ ...prevState, { role: USER_ROLE, content: value || searchTerm }, ]); setSearchTerm(""); }; return (
{ tracker.trackEvent("next", { props: { page } }); setPage((prevPage) => prevPage + 1); }} /> { tracker.trackEvent("back", { props: { page } }); setPage((prevPage) => prevPage - 1); }} />
{messages.map((message, index) => { if (message.role !== USER_ROLE) { return; } return ( ); })}
{ setShowMagicSearch(!showMagicSearch); }} >
{ setShowMagicSearch(!showMagicSearch); }} >
); }; export default MagicSearch;