diff --git a/app/assets/icons/exclamation.svg b/app/assets/icons/exclamation.svg new file mode 100644 index 00000000..3f736b9a --- /dev/null +++ b/app/assets/icons/exclamation.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/components/Article/Contents.tsx b/app/components/Article/Contents.tsx index c0ae2d26..500edb86 100644 --- a/app/components/Article/Contents.tsx +++ b/app/components/Article/Contents.tsx @@ -10,14 +10,14 @@ const footnoteHTML = (el: HTMLDivElement, e: HTMLAnchorElement): string | null = if (!footnote) return null - const elem = document.createElement('div') - elem.innerHTML = footnote.innerHTML + const elem = footnote.cloneNode(true) as Element // remove the back link, as it's useless in the popup - if (elem?.firstElementChild?.lastChild) - elem.firstElementChild.removeChild(elem.firstElementChild.lastChild) + Array.from(elem.getElementsByClassName('footnote-backref')).forEach((e) => { + e.parentElement?.removeChild(e) + }) - return elem.innerHTML + return elem.firstElementChild?.innerHTML || null } const addPopup = (e: HTMLElement, id: string, contents: string, mobile?: boolean): HTMLElement => { @@ -81,6 +81,10 @@ const glossaryInjecter = (pageid: string, glossary: Glossary) => { } const insertGlossary = (pageid: string, glossary: Glossary) => { + // Generate a random ID for these glossary items. This is needed when mulitple articles are displayed - + // gloassary items should be only displayed once per article, but this is checked by popup id, so if + // there are 2 articles that have the same glossary item, then only the first articles popups would work + const randomId = Math.floor(1000 + Math.random() * 9000).toString() const injecter = glossaryInjecter(pageid, glossary) return (textNode: Node) => { @@ -129,11 +133,11 @@ const insertGlossary = (pageid: string, glossary: Glossary) => { const image = entry.image && `` addPopup( e as HTMLSpanElement, - `glossary-${entry.term}`, + `glossary-${entry.term}-${randomId}`, `
${entry.term}
-
${entry.contents}
+
${entry.contents}
${link || ''}
${image || ''} diff --git a/app/components/Article/article.css b/app/components/Article/article.css index 2163894d..c0e60b3f 100644 --- a/app/components/Article/article.css +++ b/app/components/Article/article.css @@ -125,7 +125,7 @@ article .contents a.button { article .link-popup .glossary-popup > .contents { padding: var(--spacing-24) var(--spacing-40) var(--spacing-24); } -article .defintion { +article .definition { height: 140px; display: -webkit-box; /* These are webkit specific things, so might not work in all browsers (firefox handles them fine) */ diff --git a/app/components/Chatbot/ChatEntry.tsx b/app/components/Chatbot/ChatEntry.tsx index 3a9657d6..a716daaa 100644 --- a/app/components/Chatbot/ChatEntry.tsx +++ b/app/components/Chatbot/ChatEntry.tsx @@ -172,11 +172,17 @@ const Reference = (citation: Citation) => { ) } -const ChatbotReply = ({question, phase, content, citationsMap}: AssistantEntry) => { +const ChatbotReply = ({ + question, + phase, + content, + citationsMap, + no, +}: AssistantEntry & {no: number}) => { const mobile = useIsMobile() const citations = [] as Citation[] citationsMap?.forEach((v) => { - citations.push(v) + citations.push({...v, id: `${v.id}-${no}`}) }) citations.sort((a, b) => a.index - b.index) const phaseMessageClass = 'phase-message large-reading' @@ -216,7 +222,7 @@ const ChatbotReply = ({question, phase, content, citationsMap}: AssistantEntry) if (chunk?.match(/(\[\d+\])/)) { const refId = chunk.slice(1, chunk.length - 1) const ref = citationsMap?.get(refId) - return ref && + return ref && } else if (chunk === '\n') { return
} else { @@ -253,10 +259,15 @@ const ChatbotReply = ({question, phase, content, citationsMap}: AssistantEntry) ) } -const StampyArticle = ({pageid, content, title}: StampyEntry) => { +const StampyArticle = ({pageid, content, title, no}: StampyEntry & {no: number}) => { const glossary = useGlossary() const hint = `This response is pulled from our article "${title}" which was written by members of AISafety.info` + const uniqueReferences = (content: string, idFinder: string) => + content + .replaceAll(new RegExp(`id="(${idFinder})"`, 'g'), `id="$1-${no}"`) + .replaceAll(new RegExp(`href="#(${idFinder})"`, 'g'), `href="#$1-${no}"`) + return (
@@ -264,7 +275,7 @@ const StampyArticle = ({pageid, content, title}: StampyEntry) => { <article className="stampy"> <Contents pageid={pageid || ''} - html={content || 'Loading...'} + html={uniqueReferences(content || 'Loading...', 'fn\\d+-.*?')} glossary={glossary || {}} /> </article> diff --git a/app/components/Chatbot/chat_entry.css b/app/components/Chatbot/chat_entry.css index 45bfc7c6..8ef1b6f0 100644 --- a/app/components/Chatbot/chat_entry.css +++ b/app/components/Chatbot/chat_entry.css @@ -183,6 +183,10 @@ article.stampy { transition-delay: 0s; } +.chat-entry article { + min-height: inherit; +} + @media (max-width: 780px) { .title-inner-container { flex-direction: column; diff --git a/app/components/Chatbot/index.tsx b/app/components/Chatbot/index.tsx index 062d1dcf..25fa4c48 100644 --- a/app/components/Chatbot/index.tsx +++ b/app/components/Chatbot/index.tsx @@ -1,11 +1,13 @@ import {useEffect, useRef, useState} from 'react' import {useFetcher, useNavigate} from '@remix-run/react' +import ExclamationIcon from '../icons-generated/Exclamation' import IconStampyLarge from '~/components/icons-generated/StampyLarge' import IconStampySmall from '~/components/icons-generated/StampySmall' import SendIcon from '~/components/icons-generated/PlaneSend' import Button from '~/components/Button' import {queryLLM, Entry, AssistantEntry, StampyEntry, Followup, ChatSettings} from '~/hooks/useChat' import useOutsideOnClick from '~/hooks/useOnOutsideClick' +import useOnSiteQuestions from '~/hooks/useOnSiteQuestions' import ChatEntry from './ChatEntry' import './widgit.css' import {questionUrl} from '~/routesMapper' @@ -13,38 +15,6 @@ import {Question} from '~/server-utils/stampy' import {useSearch} from '~/hooks/useSearch' import Input from '~/components/Input' -// to be replaced with actual pool questions -const poolQuestions = [ - {text: 'Do people seriously worry about existential risk from AI?', pageid: '6953'}, - {text: 'Is AI safety about systems becoming malevolent or conscious?', pageid: '6194'}, - {text: 'When do experts think human-level AI will be created?', pageid: '5633'}, - {text: 'Why is AI alignment a hard problem?', pageid: '8163'}, - { - text: 'Why can’t we just “put the AI in a box” so that it can’t influence the outside world?', - pageid: '6176', - }, - { - text: 'What are the differences between AGI, transformative AI, and superintelligence?', - pageid: '5864', - }, - {text: 'What are large language models?', pageid: '5864'}, - {text: "Why can't we just turn the AI off if it starts to misbehave?", pageid: '3119'}, - {text: 'What is instrumental convergence?', pageid: '897I'}, - {text: "What is Goodhart's law?", pageid: '8185'}, - {text: 'What is the orthogonality thesis?', pageid: '6568'}, - {text: 'How powerful would a superintelligence become?', pageid: '7755'}, - {text: 'Will AI be able to think faster than humans?', pageid: '8E41'}, - {text: "Isn't the real concern misuse?", pageid: '9B85'}, - {text: 'Are AIs conscious?', pageid: '8V5J'}, - { - text: 'What are the differences between a singularity, an intelligence explosion, and a hard takeoff?', - pageid: '8IHO', - }, - {text: 'What is an intelligence explosion?', pageid: '6306'}, - {text: 'How might AGI kill people?', pageid: '5943'}, - {text: 'What is a "warning shot"?', pageid: '7748'}, -] - const MIN_SIMILARITY = 0.85 type QuestionInputProps = { @@ -98,7 +68,7 @@ const QuestionInput = ({ <p className="default">{results[0].title}</p> </Button> ) : undefined} - <div className="flex-container"> + <div className="relative"> <Input placeholder={placeholder} className="large full-width shadowed" @@ -115,18 +85,18 @@ const QuestionInput = ({ <SendIcon className="send pointer" onClick={() => handleAsk(question)} /> </div> {fixed && <div className="white-space"></div>} + + <div className="mobile-only grey padding-top-8"> + <ExclamationIcon /> <span>Stampy can be inaccurate. Always verify its sources.</span> + </div> </div> ) } export const WidgetStampy = ({className}: {className?: string}) => { const [question, setQuestion] = useState('') + const {selected: questions} = useOnSiteQuestions() const navigate = useNavigate() - const questions = [ - '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) => `/chat/?question=${question.trim()}` return ( @@ -140,10 +110,10 @@ export const WidgetStampy = ({className}: {className?: string}) => { <IconStampySmall /> <div className="sample-messages rounded"> <div className="padding-bottom-24">Try asking me...</div> - {questions.map((question, i) => ( + {questions.map(({title}, i) => ( <div key={i} className="padding-bottom-16"> - <Button className="secondary-alt-large" action={stampyUrl(question)}> - {question} + <Button className="secondary-alt-large" action={stampyUrl(title)}> + {title} </Button> </div> ))} @@ -171,10 +141,14 @@ type FollowupsProps = { className?: string } const Followups = ({title, followups, onSelect, className}: FollowupsProps) => { + const {randomQuestions} = useOnSiteQuestions() const items = (followups?.length || 0) >= 3 ? followups - : [...(followups || []), ...poolQuestions.sort(() => Math.random() - 0.5)].slice(0, 3) + : [ + ...(followups || []), + ...randomQuestions().map(({title, pageid}) => ({text: title, pageid})), + ].slice(0, 3) return ( <> {title && <div className={'padding-bottom-24 grey' + (className || '')}>{title}</div>} @@ -216,12 +190,11 @@ const SplashScreen = ({ type ChatbotProps = { question?: string - questions?: Followup[] + questions?: Question[] settings?: ChatSettings } export const Chatbot = ({question, questions, settings}: ChatbotProps) => { const [followups, setFollowups] = useState<Followup[]>() - const [sessionId] = useState(crypto.randomUUID()) const [history, setHistory] = useState([] as Entry[]) const [controller, setController] = useState(() => new AbortController()) @@ -337,10 +310,13 @@ export const Chatbot = ({question, questions, settings}: ChatbotProps) => { return ( <div className="centered col-10 height-70"> {history.length === 0 ? ( - <SplashScreen questions={questions} onSelection={showArticleByID} /> + <SplashScreen + questions={questions?.map(({title, pageid}) => ({pageid, text: title}))} + onSelection={showArticleByID} + /> ) : undefined} {history.map((item, i) => ( - <ChatEntry key={`chat-entry-${i}`} {...item} /> + <ChatEntry key={`chat-entry-${i}`} {...item} no={i} /> ))} <div className="padding-bottom-128"> @@ -360,9 +336,9 @@ export const Chatbot = ({question, questions, settings}: ChatbotProps) => { fixed /> - <div className={'warning-floating'}> - <p className={'xs'}> - <span className={'red xs-bold'}>Caution! </span> + <div className="desktop-only warning-floating"> + <p className="xs"> + <span className="red xs-bold">Caution! </span> This is an early prototype. Don’t automatically trust what it says, and make sure to follow its sources. </p> diff --git a/app/components/icons-generated/Exclamation.tsx b/app/components/icons-generated/Exclamation.tsx new file mode 100644 index 00000000..fb2afffd --- /dev/null +++ b/app/components/icons-generated/Exclamation.tsx @@ -0,0 +1,16 @@ +import type {SVGProps} from 'react' +const SvgExclamation = (props: SVGProps<SVGSVGElement>) => ( + <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} fill="none" {...props}> + <path + fill="#D40000" + d="M8.5 11a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0M7.5 5v4a.5.5 0 0 0 1 0V5a.5.5 0 0 0-1 0" + /> + <path + fill="#D40000" + fillRule="evenodd" + d="M8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12m0-1A5 5 0 1 0 8 3a5 5 0 0 0 0 10" + clipRule="evenodd" + /> + </svg> +) +export default SvgExclamation diff --git a/app/components/icons-generated/index.ts b/app/components/icons-generated/index.ts index 7c2e13b4..51d2f9a6 100644 --- a/app/components/icons-generated/index.ts +++ b/app/components/icons-generated/index.ts @@ -23,6 +23,7 @@ export {default as EclipseIndividual} from './EclipseIndividual' export {default as EclipseTeam} from './EclipseTeam' export {default as Edit} from './Edit' export {default as Ellipsis} from './Ellipsis' +export {default as Exclamation} from './Exclamation' export {default as Flag} from './Flag' export {default as Followup} from './Followup' export {default as GetInvolvedMobile} from './GetInvolvedMobile' diff --git a/app/hooks/useChat.ts b/app/hooks/useChat.ts index 69747950..975ef5cd 100644 --- a/app/hooks/useChat.ts +++ b/app/hooks/useChat.ts @@ -12,7 +12,7 @@ export type Citation = { id?: string } -export type Entry = UserEntry | AssistantEntry | ErrorMessage | StampyEntry +export type Entry = (UserEntry | AssistantEntry | ErrorMessage | StampyEntry) & {no?: number} export type ChatPhase = | 'started' | 'semantic' diff --git a/app/hooks/useOnSiteQuestions.ts b/app/hooks/useOnSiteQuestions.ts new file mode 100644 index 00000000..b2504492 --- /dev/null +++ b/app/hooks/useOnSiteQuestions.ts @@ -0,0 +1,44 @@ +import {Question} from '~/server-utils/stampy' +import {useOnSiteQuestions as getQuestions} from './useCachedObjects' +import {useEffect, useState} from 'react' + +const topQuestions = [ + '6953', // Do people seriously worry about existential risk from AI? debugger eval code:1:26 + '6194', // Is AI safety about systems becoming malevolent or conscious? debugger eval code:1:26 + '5633', // When do experts think human-level AI will be created? debugger eval code:1:26 + '8163', // Why is AI alignment a hard problem? debugger eval code:1:26 + '6176', // Why can’t we just “put the AI in a box” so that it can’t influence the outside world? debugger eval code:1:26 + '5864', // What are the differences between AGI, transformative AI, and superintelligence? debugger eval code:1:26 + '5864', // What are large language models? debugger eval code:1:26 + '3119', // Why can't we just turn the AI off if it starts to misbehave? debugger eval code:1:26 + '897I', // What is instrumental convergence? debugger eval code:1:26 + '8185', // What is Goodhart's law? debugger eval code:1:26 + '6568', // What is the orthogonality thesis? debugger eval code:1:26 + '7755', // How powerful would a superintelligence become? debugger eval code:1:26 + '8E41', // Will AI be able to think faster than humans? debugger eval code:1:26 + '9B85', // Isn't the real concern misuse? debugger eval code:1:26 + '8V5J', // Are AIs conscious? debugger eval code:1:26 + '8IHO', // What are the differences between a singularity, an intelligence explosion, and a hard takeoff? debugger eval code:1:26 + '6306', // What is an intelligence explosion? debugger eval code:1:26 + '5943', // How might AGI kill people? debugger eval code:1:26 + '7748', // What is a "warning shot"? +] + +const useOnSiteQuestions = () => { + const onSite = getQuestions() + const [selected, setSelected] = useState([] as Question[]) + + const top = onSite.items?.filter((i) => topQuestions.includes(i.pageid)) + const randomQuestions = (n?: number) => + top?.sort(() => 0.5 - Math.random()).slice(0, n || 3) || [] + + useEffect(() => { + if (!selected?.length) { + setSelected(randomQuestions()) + } + // eslint-disable-next-line + }, [onSite.items]) + + return {...onSite, topQuestions: top, selected, randomQuestions} +} +export default useOnSiteQuestions diff --git a/app/hooks/useSearch.tsx b/app/hooks/useSearch.tsx index 4d604c04..02a39701 100644 --- a/app/hooks/useSearch.tsx +++ b/app/hooks/useSearch.tsx @@ -1,6 +1,6 @@ import {useState, useEffect, useRef} from 'react' import {Question} from '~/server-utils/stampy' -import {useOnSiteQuestions} from './useCachedObjects' +import useOnSiteQuestions from './useOnSiteQuestions' const NUM_RESULTS = 8 diff --git a/app/hooks/useTags.ts b/app/hooks/useTags.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/app/routes/chat.tsx b/app/routes/chat.tsx index 98dfc451..a908c502 100644 --- a/app/routes/chat.tsx +++ b/app/routes/chat.tsx @@ -6,6 +6,7 @@ import Chatbot from '~/components/Chatbot' import {ChatSettings, Mode} from '~/hooks/useChat' import Button from '~/components/Button' import useOutsideOnClick from '~/hooks/useOnOutsideClick' +import useOnSiteQuestions from '~/hooks/useOnSiteQuestions' export const shouldRevalidate: ShouldRevalidateFunction = () => false @@ -15,6 +16,7 @@ export default function App() { const clickDetectorRef = useOutsideOnClick(() => setShowSettings(false)) const [chatSettings, setChatSettings] = useState({mode: 'default'} as ChatSettings) const question = params.get('question') || undefined + const {selected: questions} = useOnSiteQuestions() useEffect(() => { setChatSettings( @@ -35,15 +37,7 @@ export default function App() { return ( <Page noFooter> <div className="page-body full-height padding-top-32"> - <Chatbot - question={question} - questions={[ - {text: 'What is AI Safety?', pageid: '8486'}, - {text: 'How would the AI even get out in the world?', pageid: '8222'}, - {text: 'Do people seriously worry about existential risk from AI?', pageid: '6953'}, - ]} - settings={chatSettings} - /> + <Chatbot question={question} questions={questions} settings={chatSettings} /> <div className="settings-container z-index-1" ref={clickDetectorRef}> {showSettings && ( <div className="settings bordered flex-container"> diff --git a/app/routes/questions.$questionId.$.tsx b/app/routes/questions.$questionId.$.tsx index f497fab5..8776ca96 100644 --- a/app/routes/questions.$questionId.$.tsx +++ b/app/routes/questions.$questionId.$.tsx @@ -12,9 +12,10 @@ import {ArticlesNav} from '~/components/ArticlesNav/ArticleNav' import {QuestionStatus, loadQuestionDetail} from '~/server-utils/stampy' import useToC from '~/hooks/useToC' import useGlossary from '~/hooks/useGlossary' +import useOnSiteQuestions from '~/hooks/useOnSiteQuestions' +import {useTags} from '~/hooks/useCachedObjects' import type {Question, Tag} from '~/server-utils/stampy' import {reloadInBackgroundIfNeeded} from '~/server-utils/kv-cache' -import {useOnSiteQuestions, useTags} from '~/hooks/useCachedObjects' export const LINK_WITHOUT_DETAILS_CLS = 'link-without-details'