From 88ded6ffa22caacfe5d5464399b6a2852f0c20b3 Mon Sep 17 00:00:00 2001 From: Daniel O'Connell Date: Mon, 29 Apr 2024 22:41:05 +0200 Subject: [PATCH] use human answers when available --- app/components/Chatbot/ChatEntry.tsx | 74 ++++++++++++++++-------- app/components/Chatbot/Widgit.tsx | 57 ------------------- app/components/Chatbot/chat_entry.css | 81 +++++++++++++++++++++++++-- app/components/Chatbot/index.tsx | 76 ++++++++++++++++++++----- app/components/Page.tsx | 10 +++- app/components/search.tsx | 17 +----- app/hooks/useChat.ts | 31 +++++----- app/hooks/useSearch.tsx | 71 ++++++++++++++++++----- app/routes/_index.tsx | 3 +- app/routes/chat.tsx | 10 +++- stories/WidgetStampy.stories.tsx | 2 +- 11 files changed, 285 insertions(+), 147 deletions(-) delete mode 100644 app/components/Chatbot/Widgit.tsx diff --git a/app/components/Chatbot/ChatEntry.tsx b/app/components/Chatbot/ChatEntry.tsx index 1cfc9499..c28a2d9d 100644 --- a/app/components/Chatbot/ChatEntry.tsx +++ b/app/components/Chatbot/ChatEntry.tsx @@ -1,5 +1,6 @@ import {ComponentType} from 'react' import {Link} from '@remix-run/react' +import MarkdownIt from 'markdown-it' import QuestionMarkIcon from '~/components/icons-generated/QuestionMark' import BotIcon from '~/components/icons-generated/Bot' import PersonIcon from '~/components/icons-generated/Person' @@ -7,15 +8,17 @@ import StampyIcon from '~/components/icons-generated/Stampy' import Contents from '~/components/Article/Contents' import useGlossary from '~/hooks/useGlossary' import './chat_entry.css' -import type {Entry, AssistantEntry, StampyEntry, Citation} from '~/hooks/useChat' +import type {Entry, AssistantEntry, StampyEntry, Citation, ErrorMessage} from '~/hooks/useChat' +const MAX_REFERENCES = 10 const hints = { bot: 'bla bla bla something bot', human: 'bla bla bla by humans', + error: null, } -const AnswerInfo = ({answerType}: {answerType?: 'human' | 'bot'}) => { - if (!answerType) return null +const AnswerInfo = ({answerType}: {answerType?: 'human' | 'bot' | 'error'}) => { + if (!answerType || !hints[answerType]) return null return ( {answerType === 'human' ? : } @@ -31,7 +34,7 @@ const AnswerInfo = ({answerType}: {answerType?: 'human' | 'bot'}) => { type TitleProps = { title: string Icon: ComponentType - answerType?: 'human' | 'bot' + answerType?: 'human' | 'bot' | 'error' } const Title = ({title, Icon, answerType}: TitleProps) => (
@@ -48,14 +51,29 @@ const UserQuery = ({content}: Entry) => (
) -// FIXME: this id should be unique across the page - I doubt it will be now -const ReferenceLink = ({id, reference}: {id: string; reference: string}) => ( - - {reference} - -) +const md = new MarkdownIt({html: true}) +const ReferenceLink = ({id, index, text}: Citation) => { + if (!index || index > MAX_REFERENCES) return '' -const Reference = ({id, title, authors, source, url, reference}: Citation) => { + const parsed = text?.match(/^###.*?###\s+"""(.*?)"""$/ms) + return ( + <> + + {index} + + {parsed && ( +
+ )} + + ) +} + +const Reference = ({id, title, authors, source, url, index}: Citation) => { const referenceSources = { arxiv: 'Scientific paper', blogs: 'Blogpost', @@ -65,6 +83,7 @@ const Reference = ({id, title, authors, source, url, reference}: Citation) => { arbital: 'Arbital', distill: 'Distill', 'aisafety.info': 'AISafety.info', + youtube: 'YouTube', } const Authors = ({authors}: {authors?: string[]}) => { @@ -78,8 +97,8 @@ const Reference = ({id, title, authors, source, url, reference}: Citation) => { } return ( -
-
{reference}
+
+
{index}
{title}
@@ -99,9 +118,7 @@ const ChatbotReply = ({phase, content, citationsMap}: AssistantEntry) => { citationsMap?.forEach((v) => { citations.push(v) }) - - const references = citations.map(({reference}) => reference).join('') - const referencesRegex = new RegExp(`(\\[[${references}]\\])`) + citations.sort((a, b) => a.index - b.index) const PhaseState = () => { switch (phase) { @@ -128,17 +145,20 @@ const ChatbotReply = ({phase, content, citationsMap}: AssistantEntry) => {
<PhaseState /> - <div> - {content?.split(referencesRegex).map((chunk, i) => { - if (chunk.match(referencesRegex)) { - const ref = citationsMap?.get(chunk[1]) - return <ReferenceLink key={i} id={ref?.id || chunk[i]} reference={chunk[1]} /> + <div className="padding-bottom-24"> + {content?.split(/(\[\d+\])|(\n)/).map((chunk, i) => { + if (chunk?.match(/(\[\d+\])/)) { + const refId = chunk.slice(1, chunk.length - 1) + const ref = citationsMap?.get(refId) + return ref && <ReferenceLink key={i} {...ref} /> + } else if (chunk === '\n') { + return <br key={i} /> } else { return <span key={i}>{chunk}</span> } })} </div> - {citations?.map(Reference)} + {citations?.slice(0, MAX_REFERENCES).map(Reference)} {phase === 'followups' ? <p>Checking for followups...</p> : undefined} </div> ) @@ -157,11 +177,21 @@ const StampyArticle = ({pageid, content}: StampyEntry) => { ) } +const ErrorReply = ({content}: ErrorMessage) => { + return ( + <div> + <Title title="Error" Icon={StampyIcon} answerType="error" /> + <div>{content}</div> + </div> + ) +} + const ChatEntry = (props: Entry) => { const roles = { user: UserQuery, stampy: StampyArticle, assistant: ChatbotReply, + error: ErrorReply, } as {[k: string]: ComponentType<Entry>} const Role = roles[props.role] as ComponentType<Entry> if (!Role) return null diff --git a/app/components/Chatbot/Widgit.tsx b/app/components/Chatbot/Widgit.tsx deleted file mode 100644 index 1c71e10f..00000000 --- a/app/components/Chatbot/Widgit.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import {useState} from 'react' -import {Link} from '@remix-run/react' -import StampyIcon from '~/components/icons-generated/Stampy' -import SendIcon from '~/components/icons-generated/PlaneSend' -import Button from '~/components/Button' -import './widgit.css' - -export const WidgetStampy = () => { - const [question, setQuestion] = useState('') - const questions = [ - 'Why couldn’t we just turn the AI off?', - 'How would the AI even get out in the world?', - 'Do people seriously worry about existential risk from AI?', - ] - - const stampyUrl = (question: string) => `https://chat.aisafety.info/?question=${question.trim()}` - return ( - <div className="centered col-9 padding-bottom-128"> - <div className="col-6 padding-bottom-56"> - <h2 className="teal-500">Questions?</h2> - <h2>Ask Stampy, our chatbot, any question about AI safety</h2> - </div> - - <div className="sample-messages-container padding-bottom-24"> - <StampyIcon /> - <div className="sample-messages rounded"> - <div className="padding-bottom-24">Try asking me...</div> - {questions.map((question, i) => ( - <div key={i} className="padding-bottom-16"> - <Button className="secondary-alt" action={stampyUrl(question)}> - {question} - </Button> - </div> - ))} - </div> - </div> - - <div className="widget-ask"> - <input - type="text" - className="full-width bordered secondary" - placeholder="Ask Stampy a question..." - value={question} - onChange={(e) => setQuestion(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' && question.trim()) { - window.location = stampyUrl(question) as any - } - }} - /> - <Link to={stampyUrl(question)}> - <SendIcon /> - </Link> - </div> - </div> - ) -} diff --git a/app/components/Chatbot/chat_entry.css b/app/components/Chatbot/chat_entry.css index 605ba382..4d40fbe1 100644 --- a/app/components/Chatbot/chat_entry.css +++ b/app/components/Chatbot/chat_entry.css @@ -30,10 +30,66 @@ article.stampy { } .chat-entry .reference-link { - background: red; - width: var(--spacing-16); - height: var(--spacing-16); + font-size: smaller; + vertical-align: super; + padding: 2px 4px; + margin-left: 2px; + border-radius: var(--border-radius); +} +.chat-entry .reference-link span { + min-width: 10px; display: inline-block; + text-align: center; +} + +.ref-1 { + background: rgb(211, 255, 253); + color: rgb(24, 185, 71); +} + +.ref-2 { + background: rgb(255, 221, 244); + color: rgb(251, 0, 158); +} + +.ref-3 { + background: rgb(217, 253, 254); + color: rgb(23, 184, 197); +} + +.ref-4 { + background: rgb(254, 230, 202); + color: rgb(230, 107, 9); +} + +.ref-5 { + background: rgb(244, 223, 255); + color: rgb(164, 3, 254); +} + +.ref-6 { + background: rgb(231, 255, 178); + color: rgb(99, 159, 4); +} + +.ref-7 { + background: rgb(231, 232, 255); + color: rgb(77, 75, 254); +} + +.ref-8 { + background: rgb(255, 254, 156); + color: rgb(144, 140, 5); +} + +.ref-9 { + background: rgb(254, 226, 226); + color: rgb(200, 0, 5); +} + +.ref-10 { + background: rgb(214, 240, 255); + color: rgb(18, 144, 254); } .reference { @@ -43,7 +99,6 @@ article.stampy { .reference .reference-num { width: var(--spacing-32); height: var(--spacing-32); - background: red; border-radius: 6px; text-align: center; } @@ -51,3 +106,21 @@ article.stampy { .reference .source-link { color: var(--colors-teal-500); } + +.reference-contents { + visibility: hidden; + transition: visibility 0.2s; + max-width: 600px; + word-wrap: break-word; + background-color: var(--colors-cool-grey-300); + padding: var(--spacing-16) var(--spacing-24); + position: absolute; + transform: translateX(50%); + text-decoration: unset; +} + +.reference-contents:hover, +.reference-link:hover + .reference-contents { + visibility: visible; + transition-delay: 0s; +} diff --git a/app/components/Chatbot/index.tsx b/app/components/Chatbot/index.tsx index 8dd8292d..68682070 100644 --- a/app/components/Chatbot/index.tsx +++ b/app/components/Chatbot/index.tsx @@ -1,4 +1,4 @@ -import {useEffect, useState} from 'react' +import {useEffect, useRef, useState} from 'react' import {Link, useFetcher} from '@remix-run/react' import StampyIcon from '~/components/icons-generated/Stampy' import SendIcon from '~/components/icons-generated/PlaneSend' @@ -8,16 +8,17 @@ import ChatEntry from './ChatEntry' import './widgit.css' import {questionUrl} from '~/routesMapper' import {Question} from '~/server-utils/stampy' +import {useSearch} from '~/hooks/useSearch' export const WidgetStampy = () => { const [question, setQuestion] = useState('') const questions = [ - 'Why couldn’t we just turn the AI off?', + 'What is AI Safety?', 'How would the AI even get out in the world?', 'Do people seriously worry about existential risk from AI?', ] - const stampyUrl = (question: string) => `https://chat.aisafety.info/?question=${question.trim()}` + const stampyUrl = (question: string) => `/chat/?question=${question.trim()}` return ( <div className="centered col-9 padding-bottom-128"> <div className="col-6 padding-bottom-56"> @@ -68,6 +69,11 @@ type QuestionInputProps = { const QuestionInput = ({initial, onChange, onAsk}: QuestionInputProps) => { const [question, setQuestion] = useState(initial || '') + const handleAsk = (val: string) => { + onAsk && onAsk(val) + setQuestion('') + } + const handleChange = (val: string) => { setQuestion(val) onChange && onChange(val) @@ -83,11 +89,11 @@ const QuestionInput = ({initial, onChange, onAsk}: QuestionInputProps) => { onChange={(e) => handleChange(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && question.trim() && onAsk) { - onAsk(question) + handleAsk(question) } }} /> - <SendIcon className="pointer" onClick={() => onAsk && onAsk(question)} /> + <SendIcon className="pointer" onClick={() => handleAsk(question)} /> </div> ) } @@ -135,10 +141,12 @@ const SplashScreen = ({ export const Chatbot = ({question, questions}: {question?: string; questions?: string[]}) => { const [followups, setFollowups] = useState<Followup[]>() + // FIXME: Generate session id const [sessionId] = useState('asd') const [history, setHistory] = useState([] as Entry[]) const [controller, setController] = useState(() => new AbortController()) const fetcher = useFetcher({key: 'followup-fetcher'}) + const {search, resultsForRef, waitForResults} = useSearch(1) useEffect(() => { if (!fetcher.data || fetcher.state !== 'idle') return @@ -177,28 +185,68 @@ export const Chatbot = ({question, questions}: {question?: string; questions?: s setFollowups(undefined) } - const abortSearch = () => controller.abort() // eslint-disable-line @typescript-eslint/no-unused-vars - const onQuestion = async (question: string) => { + // Cancel any previous queries + controller.abort() + const newController = new AbortController() + setController(newController) + + // Add a new history entry, replacing the previous one if it was canceled const message = {content: question, role: 'user'} as Entry - const controller = new AbortController() - setController(controller) + setHistory((current) => { + const last = current[current.length - 1] + if ( + (last?.role === 'assistant' && ['streaming', 'followups'].includes(last?.phase || '')) || + (last?.role === 'stampy' && last?.content) || + ['error'].includes(last?.role) + ) { + return [...current, message, {role: 'assistant'} as AssistantEntry] + } else if (last?.role === 'user' && last?.content === question) { + return [...current.slice(0, current.length - 1), {role: 'assistant'} as AssistantEntry] + } + return [ + ...current.slice(0, current.length - 2), + message, + {role: 'assistant'} as AssistantEntry, + ] + }) setFollowups(undefined) - setHistory((current) => [...current, message, {role: 'assistant'} as AssistantEntry]) const updateReply = (reply: Entry) => setHistory((current) => [...current.slice(0, current.length - 2), message, reply]) + search(question) + const [humanWritten] = await waitForResults(100, 1000) + if (newController.signal.aborted) { + return + } + + if (humanWritten && humanWritten.score > 0.85 && question === resultsForRef.current) { + fetcher.load(questionUrl({pageid: humanWritten.pageid})) + updateReply({pageid: humanWritten.pageid, role: 'stampy'} as StampyEntry) + return + } + const {followups, result} = await queryLLM( [...history, message], updateReply, sessionId, - controller + newController ) - updateReply(result) - setFollowups(followups) + if (!newController.signal.aborted) { + updateReply(result) + setFollowups(followups) + } } + const fetchFlag = useRef(false) + useEffect(() => { + if (question && !fetchFlag.current) { + fetchFlag.current = true + onQuestion(question) + } + }) + return ( <div className="centered col-9 padding-bottom-128"> {history.length === 0 ? ( @@ -214,7 +262,7 @@ export const Chatbot = ({question, questions}: {question?: string; questions?: s onSelect={showFollowup} /> ) : undefined} - <QuestionInput initial={question} onAsk={onQuestion} /> + <QuestionInput onAsk={onQuestion} /> </div> ) } diff --git a/app/components/Page.tsx b/app/components/Page.tsx index 72e10598..dba38cce 100644 --- a/app/components/Page.tsx +++ b/app/components/Page.tsx @@ -7,7 +7,13 @@ import MobileNav from '~/components/Nav/Mobile' import {useTags} from '~/hooks/useCachedObjects' import useToC from '~/hooks/useToC' import useIsMobile from '~/hooks/isMobile' -const Page = ({children, modal}: {children: ReactNode; modal?: boolean}) => { + +type PageProps = { + children: ReactNode + modal?: boolean + noFooter?: boolean +} +const Page = ({children, modal, noFooter}: PageProps) => { const {toc} = useToC() const {items: tags} = useTags() const {embed} = useOutletContext<Context>() || {} @@ -22,7 +28,7 @@ const Page = ({children, modal}: {children: ReactNode; modal?: boolean}) => { ))} {children} - {!embed && !modal && <Footer />} + {!embed && !modal && !noFooter && <Footer />} </> ) } diff --git a/app/components/search.tsx b/app/components/search.tsx index 42ace05d..dcb42afe 100644 --- a/app/components/search.tsx +++ b/app/components/search.tsx @@ -1,10 +1,8 @@ import {useState, useEffect, useRef} from 'react' import debounce from 'lodash/debounce' import {useSearch} from '~/hooks/useSearch' -import {Question} from '~/server-utils/stampy' import {SearchInput} from './SearchInput/Input' import {SearchResults} from './SearchResults/Dropdown' -import {fetchAllQuestionsOnSite} from '~/routes/questions.allQuestionsOnSite' import {questionUrl} from '~/routesMapper' type Props = { @@ -13,24 +11,11 @@ type Props = { removeQueryFromUrl?: () => void } -const empty: [] = [] - export default function Search({queryFromUrl, limitFromUrl, removeQueryFromUrl}: Props) { const [showResults, setShowResults] = useState(!!queryFromUrl) const searchInputRef = useRef('') - const onSiteAnswersRef = useRef<Question[]>(empty) - useEffect(() => { - // not needed for initial screen => lazy load on client - fetchAllQuestionsOnSite().then(({data, backgroundPromiseIfReloaded}) => { - onSiteAnswersRef.current = data - backgroundPromiseIfReloaded.then((x) => { - if (x) onSiteAnswersRef.current = x.data - }) - }) - }, []) - - const {search, isPendingSearch, results} = useSearch(onSiteAnswersRef, limitFromUrl) + const {search, isPendingSearch, results} = useSearch(limitFromUrl) const searchFn = (rawValue: string) => { const value = rawValue.trim() diff --git a/app/hooks/useChat.ts b/app/hooks/useChat.ts index c99f54ec..a8563ad6 100644 --- a/app/hooks/useChat.ts +++ b/app/hooks/useChat.ts @@ -1,4 +1,5 @@ -export const CHATBOT_URL = 'https://chat.stampy.ai:8443/chat' +// export const CHATBOT_URL = 'https://chat.stampy.ai:8443/chat' +export const CHATBOT_URL = 'http://127.0.0.1:3001/chat' export type Citation = { title: string @@ -77,9 +78,9 @@ export const formatCitations: (text: string) => string = (text) => { // well to almost everything the LLM emits. We won't ever reach five nines, // but the domain is one where occasionally failing isn't catastrophic. - // transform all things that look like [a, b, c] into [a][b][c] + // transform all things that look like [1, 2, 3] into [1][2][3] let response = text.replace( - /\[((?:[a-z]+,\s*)*[a-z]+)\]/g, // identify groups of this form + /\[((?:\d+,\s*)*\d+)\]/g, // identify groups of this form (block: string) => block @@ -88,9 +89,9 @@ export const formatCitations: (text: string) => string = (text) => { .join('][') ) - // transform all things that look like [(a), (b), (c)] into [(a)][(b)][(c)] + // transform all things that look like [(1), (2), (3)] into [(1)][(2)][(3)] response = response.replace( - /\[((?:\([a-z]+\),\s*)*\([a-z]+\))\]/g, // identify groups of this form + /\[((?:\(\d+\),\s*)*\(\d+\))\]/g, // identify groups of this form (block: string) => block @@ -99,11 +100,11 @@ export const formatCitations: (text: string) => string = (text) => { .join('][') ) - // transform all things that look like [(a)] into [a] - response = response.replace(/\[\(([a-z]+)\)\]/g, (_match: string, x: string) => `[${x}]`) + // transform all things that look like [(3)] into [3] + response = response.replace(/\[\((\d+)\)\]/g, (_match: string, x: string) => `[${x}]`) - // transform all things that look like [ a ] into [a] - response = response.replace(/\[\s*([a-z]+)\s*\]/g, (_match: string, x: string) => `[${x}]`) + // transform all things that look like [ 12 ] into [12] + response = response.replace(/\[\s*(\d+)\s*\]/g, (_match: string, x: string) => `[${x}]`) return response } @@ -113,20 +114,22 @@ export const findCitations: (text: string, citations: Citation[]) => Map<string, ) => { // figure out what citations are in the response, and map them appropriately const cite_map = new Map<string, Citation>() + let index = 1 // scan a regex for [x] over the response. If x isn't in the map, add it. // (note: we're actually doing this twice - once on parsing, once on render. // if that looks like a problem, we could swap from strings to custom ropes). - const regex = /\[([a-z]+)\]/g + const regex = /\[(\d+)\]/g let match while ((match = regex.exec(text)) !== null) { - const letter = match[1] - if (!letter || cite_map.has(letter!)) continue + const ref = match[1] + if (!ref || cite_map.has(ref!)) continue - const citation = citations[letter.charCodeAt(0) - 'a'.charCodeAt(0)] + const citation = citations[parseInt(ref, 10)] if (!citation) continue - cite_map.set(letter!, citation) + cite_map.set(ref!, {...citation, index}) + index++ } return cite_map } diff --git a/app/hooks/useSearch.tsx b/app/hooks/useSearch.tsx index d109b626..63c82a33 100644 --- a/app/hooks/useSearch.tsx +++ b/app/hooks/useSearch.tsx @@ -1,4 +1,5 @@ -import {useState, useEffect, useRef, MutableRefObject} from 'react' +import {useState, useEffect, useRef} from 'react' +import {fetchAllQuestionsOnSite} from '~/routes/questions.allQuestionsOnSite' import {Question} from '~/server-utils/stampy' const NUM_RESULTS = 8 @@ -21,6 +22,23 @@ export type WorkerMessage = userQuery?: string } +const waitForCondition = async (conditionFn: () => boolean, interval = 100, timeout = 5000) => { + const startTime = Date.now() + + async function loop() { + // If the condition is met, resolve. + if (conditionFn()) return true + + // If the timeout has elapsed, reject. + if (Date.now() - startTime > timeout) throw new Error('Timeout waiting for condition') + + // Wait for the specified interval, then try again. + await new Promise((resolve) => setTimeout(resolve, interval)) + return loop() + } + return loop() +} + /** * Sort function for the highest score on top */ @@ -112,16 +130,26 @@ const normalize = (question: string) => * use baseline search over the list of questions already loaded on the site. * Searches containing only one or two words will also use the baseline search */ -export const useSearch = ( - onSiteQuestions: MutableRefObject<Question[]>, - numResults = NUM_RESULTS -) => { +export const useSearch = (numResults = NUM_RESULTS) => { const tfWorkerRef = useRef<Worker>() const runningQueryRef = useRef<string>() // detect current query in search function from previous render => ref const timeoutRef = useRef<ReturnType<typeof setTimeout>>() // cancel previous timeout => ref - const [isPendingSearch, setIsPendingSearch] = useState(false) // re-render loading indicator => state + const isPendingSearch = useRef(false) + const resultsRef = useRef<SearchResult[]>([]) + const resultsForRef = useRef<string>() const [results, setResults] = useState([] as SearchResult[]) + const onSiteAnswersRef = useRef<Question[]>([]) + useEffect(() => { + // not needed for initial screen => lazy load on client + fetchAllQuestionsOnSite().then(({data, backgroundPromiseIfReloaded}) => { + onSiteAnswersRef.current = data + backgroundPromiseIfReloaded.then((x) => { + if (x) onSiteAnswersRef.current = x.data + }) + }) + }, []) + useEffect(() => { const makeWorker = async () => { const worker = new Worker('/tfWorker.js') @@ -130,8 +158,12 @@ export const useSearch = ( tfWorkerRef.current = worker } else if (data.userQuery == runningQueryRef.current) { runningQueryRef.current = undefined - if (data.searchResults) setResults(data.searchResults) - setIsPendingSearch(false) + if (data.searchResults) { + resultsRef.current = data.searchResults + resultsForRef.current = data.userQuery + setResults(data.searchResults) + } + isPendingSearch.current = false } }) } @@ -148,7 +180,7 @@ export const useSearch = ( } const search = (userQuery: string) => { - setIsPendingSearch(true) + isPendingSearch.current = true const wordCount = userQuery.split(' ').length if (wordCount > 2) { if (runningQueryRef.current || !tfWorkerRef.current) { @@ -158,22 +190,35 @@ export const useSearch = ( runningQueryRef.current = userQuery tfWorkerRef.current.postMessage({userQuery, numResults}) } else { - if (runningQueryRef.current || onSiteQuestions.current.length == 0) { + if (runningQueryRef.current || onSiteAnswersRef.current.length == 0) { searchLater(userQuery) return } runningQueryRef.current = userQuery - baselineSearch(userQuery, onSiteQuestions.current, numResults).then((searchResults) => { + baselineSearch(userQuery, onSiteAnswersRef.current, numResults).then((searchResults) => { runningQueryRef.current = undefined + resultsRef.current = searchResults + resultsForRef.current = userQuery + isPendingSearch.current = false setResults(searchResults) - setIsPendingSearch(false) }) } } + const waitForResults = async (interval?: number, timeout?: number) => { + try { + await waitForCondition(() => !isPendingSearch.current, interval, timeout) + } catch (e) { + console.info('Timeout') + } + return resultsRef.current + } + return { search, results, - isPendingSearch, + resultsForRef, + isPendingSearch: isPendingSearch.current, + waitForResults, } } diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 36293711..a32183f5 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -7,6 +7,7 @@ import Grid from '~/components/Grid' import Page from '~/components/Page' import {getStateEntries} from '~/hooks/stateModifiers' import {questionUrl} from '~/routesMapper' +import {WidgetStampy} from '~/components/Chatbot' export const loader = async ({request}: Parameters<LoaderFunction>[0]) => { const url = new URL(request.url) @@ -38,7 +39,7 @@ export default function App() { <ContentBoxThird /> <div className="desktop-only padding-bottom-56" /> - {/* <WidgetStampy /> */} + <WidgetStampy /> <h3 className="grey large-bold padding-bottom-32">Advanced sections</h3> <Grid gridBoxes={advanced} /> diff --git a/app/routes/chat.tsx b/app/routes/chat.tsx index 23d17520..1adef68e 100644 --- a/app/routes/chat.tsx +++ b/app/routes/chat.tsx @@ -1,16 +1,20 @@ -import {ShouldRevalidateFunction} from '@remix-run/react' +import {ShouldRevalidateFunction, useSearchParams} from '@remix-run/react' import Page from '~/components/Page' import Chatbot from '~/components/Chatbot' export const shouldRevalidate: ShouldRevalidateFunction = () => false export default function App() { + const [params] = useSearchParams() + const question = params.get('question') || undefined + return ( - <Page> + <Page noFooter> <div className="page-body"> <Chatbot + question={question} questions={[ - 'Why couldn’t we just turn the AI off?', + 'What is AI Safety?', 'How would the AI even get out in the world?', 'Do people seriously worry about existential risk from AI?', ]} diff --git a/stories/WidgetStampy.stories.tsx b/stories/WidgetStampy.stories.tsx index 673c1e38..75731d73 100644 --- a/stories/WidgetStampy.stories.tsx +++ b/stories/WidgetStampy.stories.tsx @@ -1,5 +1,5 @@ import type {Meta, StoryObj} from '@storybook/react' -import {WidgetStampy} from '../app/components/Chatbot/Widgit' +import {WidgetStampy} from '../app/components/Chatbot' const meta = { title: 'Components/WidgetStampy',