Skip to content

Commit

Permalink
Merge pull request #603 from StampyAI/chat-fixes
Browse files Browse the repository at this point in the history
Chat settings
  • Loading branch information
melissasamworth authored Apr 30, 2024
2 parents c55d731 + 97ab76b commit 37d404d
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 43 deletions.
4 changes: 2 additions & 2 deletions app/components/Article/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const ArticleFooter = (question: Question) => {
<Button
className="secondary"
action={question.answerEditLink || ''}
tooltip="Edit article"
tooltip="Suggest changes in Google Docs"
props={{target: '_blank', rel: 'noopener noreferrer'}}
>
<EditIcon className="no-fill" />
Expand Down Expand Up @@ -107,7 +107,7 @@ const ArticleActions = ({answerEditLink}: Question) => {
<Button
className="secondary"
action={answerEditLink || ''}
tooltip="Edit article"
tooltip="Suggest changes in Google Docs"
props={{target: '_blank', rel: 'noopener noreferrer'}}
>
<EditIcon className="no-fill" />
Expand Down
13 changes: 10 additions & 3 deletions app/components/Chatbot/ChatEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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 LinkIcon from '~/components/icons-generated/Link'
import PersonIcon from '~/components/icons-generated/Person'
import StampyIcon from '~/components/icons-generated/Stampy'
import Contents from '~/components/Article/Contents'
Expand Down Expand Up @@ -104,8 +105,9 @@ const Reference = ({id, title, authors, source, url, index}: Citation) => {
<div>
<Authors authors={authors} />
<span>{' · '}</span>
<Link className="source-link" to={url}>
{referenceSources[source as keyof typeof referenceSources] || new URL(url).host}
<Link className="source-link" to={url} target="_blank" rel="noopener noreferrer">
{referenceSources[source as keyof typeof referenceSources] || new URL(url).host}{' '}
<LinkIcon width="16" height="16" />
</Link>
</div>
</div>
Expand Down Expand Up @@ -158,7 +160,12 @@ const ChatbotReply = ({phase, content, citationsMap}: AssistantEntry) => {
}
})}
</div>
{citations?.slice(0, MAX_REFERENCES).map(Reference)}
{citations && citations.length > 0 && (
<>
<hr />
<div className="padding-top-32">{citations?.slice(0, MAX_REFERENCES).map(Reference)}</div>
</>
)}
{phase === 'followups' ? <p>Checking for followups...</p> : undefined}
</div>
)
Expand Down
12 changes: 9 additions & 3 deletions app/components/Chatbot/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {Link, useFetcher} from '@remix-run/react'
import StampyIcon from '~/components/icons-generated/Stampy'
import SendIcon from '~/components/icons-generated/PlaneSend'
import Button from '~/components/Button'
import {queryLLM, Entry, AssistantEntry, StampyEntry, Followup} from '~/hooks/useChat'
import {queryLLM, Entry, AssistantEntry, StampyEntry, Followup, ChatSettings} from '~/hooks/useChat'
import ChatEntry from './ChatEntry'
import './widgit.css'
import {questionUrl} from '~/routesMapper'
Expand Down Expand Up @@ -138,7 +138,12 @@ const SplashScreen = ({
</>
)

export const Chatbot = ({question, questions}: {question?: string; questions?: string[]}) => {
type ChatbotProps = {
question?: string
questions?: string[]
settings?: ChatSettings
}
export const Chatbot = ({question, questions, settings}: ChatbotProps) => {
const [followups, setFollowups] = useState<Followup[]>()

// FIXME: Generate session id
Expand Down Expand Up @@ -231,7 +236,8 @@ export const Chatbot = ({question, questions}: {question?: string; questions?: s
[...history, message],
updateReply,
sessionId,
newController
newController,
settings
)
if (!newController.signal.aborted) {
updateReply(result)
Expand Down
13 changes: 13 additions & 0 deletions app/components/Chatbot/widgit.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,16 @@
width: 100%;
}
}

.settings-container {
position: absolute;
bottom: var(--spacing-16);
left: var(--spacing-16);
}

.settings {
padding: var(--spacing-32);
margin-bottom: var(--spacing-24);
flex-direction: column;
gap: var(--spacing-24);
}
20 changes: 16 additions & 4 deletions app/hooks/useCachedObjects.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {useEffect, useState, createContext, useContext, ReactElement} from 'react'
import type {Tag, Glossary} from '~/server-utils/stampy'
import type {Tag, Glossary, Question} from '~/server-utils/stampy'
import {fetchTags} from '~/routes/categories.all'
import {fetchTOC, TOCItem} from '~/routes/questions.toc'
import {fetchGlossary} from '~/routes/questions.glossary'
import {fetchAllQuestionsOnSite} from '~/routes/questions.allQuestionsOnSite'

type ServerObject = Tag[] | TOCItem[] | Glossary
type APICall = () => Promise<Tag[] | TOCItem[] | Glossary>
type ServerObject = Tag[] | TOCItem[] | Glossary | Question[]
type APICall = () => Promise<ServerObject>
type useObjectsType<T extends ServerObject> = {
items?: T
}
Expand All @@ -25,23 +26,26 @@ export const useItemsFuncs = <T extends ServerObject>(apiFetcher: APICall): useO
}

type useCachedObjectsType = {
onSiteQuestions: useObjectsType<Question[]>
glossary: useObjectsType<Glossary>
tags: useObjectsType<Tag[]>
toc: useObjectsType<TOCItem[]>
}
export const CachedObjectsContext = createContext<useCachedObjectsType | null>(null)

const getOnSiteQuestions = async () => (await fetchAllQuestionsOnSite()).data
const getGlossary = async () => (await fetchGlossary()).data
const getTags = async () => (await fetchTags()).tags
const getToC = async () => (await fetchTOC()).data

export const CachedObjectsProvider = ({children}: {children: ReactElement}) => {
const onSiteQuestions = useItemsFuncs<Question[]>(getOnSiteQuestions)
const glossary = useItemsFuncs<Glossary>(getGlossary)
const tags = useItemsFuncs<Tag[]>(getTags)
const toc = useItemsFuncs<TOCItem[]>(getToC)

return (
<CachedObjectsContext.Provider value={{tags, glossary, toc}}>
<CachedObjectsContext.Provider value={{tags, glossary, toc, onSiteQuestions}}>
{children}
</CachedObjectsContext.Provider>
)
Expand All @@ -55,6 +59,14 @@ export const useCachedObjects = () => {
return context
}

export const useOnSiteQuestions = () => {
const context = useContext(CachedObjectsContext)
if (!context) {
throw new Error('useOnSiteQuestions must be used within a CachedObjectsProvider')
}
return context.onSiteQuestions
}

export const useTags = () => {
const context = useContext(CachedObjectsContext)
if (!context) {
Expand Down
23 changes: 18 additions & 5 deletions app/hooks/useChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,18 @@ export type SearchResult = {
result: Entry
}

export type Mode = 'rookie' | 'concise' | 'default' | 'discord'
type Model =
| 'gpt-3.5-turbo'
| 'gpt-4'
| 'gpt-4-turbo-preview'
| 'claude-3-opus-20240229'
| 'claude-3-sonnet-20240229'
| 'claude-3-haiku-20240307'
export type Mode = 'rookie' | 'concise' | 'default'
export type ChatSettings = {
mode?: Mode
completions?: Model
}

const DATA_HEADER = 'data: '
const EVENT_END_HEADER = 'event: close'
Expand Down Expand Up @@ -229,7 +240,8 @@ export const extractAnswer = async (
const fetchLLM = async (
sessionId: string | undefined,
history: HistoryEntry[],
controller: AbortController
controller: AbortController,
settings?: ChatSettings
): Promise<Response | void> =>
fetch(CHATBOT_URL, {
signal: controller.signal,
Expand All @@ -240,18 +252,19 @@ const fetchLLM = async (
'Content-Type': 'application/json',
Accept: 'text/event-stream',
},
body: JSON.stringify({sessionId, history, settings: {mode: 'default'}}),
body: JSON.stringify({sessionId, history, settings}),
}).catch(ignoreAbort)

export const queryLLM = async (
history: HistoryEntry[],
setCurrent: (e: AssistantEntry) => void,
sessionId: string | undefined,
controller: AbortController
controller: AbortController,
settings?: ChatSettings
): Promise<SearchResult> => {
setCurrent({...makeEntry(), phase: 'started'})
// do SSE on a POST request.
const res = await fetchLLM(sessionId, history, controller)
const res = await fetchLLM(sessionId, history, controller, settings)

if (!res) {
return {result: {role: 'error', content: 'No response from server'}}
Expand Down
4 changes: 4 additions & 0 deletions app/root.css
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,10 @@ svg {
cursor: pointer;
}

.full-height {
height: 100%;
}

/* for troubleshooting */

.pink {
Expand Down
40 changes: 39 additions & 1 deletion app/routes/chat.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,62 @@
import {useState} from 'react'
import {ShouldRevalidateFunction, useSearchParams} from '@remix-run/react'
import SettingsIcon from '~/components/icons-generated/Settings'
import Page from '~/components/Page'
import Chatbot from '~/components/Chatbot'
import {ChatSettings, Mode} from '~/hooks/useChat'
import Button from '~/components/Button'

export const shouldRevalidate: ShouldRevalidateFunction = () => false

export default function App() {
const [params] = useSearchParams()
const [showSettings, setShowSettings] = useState(false)
const [chatSettings, setChatSettings] = useState({mode: 'default'} as ChatSettings)
const question = params.get('question') || undefined

const ModeButton = ({name, mode}: {name: string; mode: Mode}) => (
<Button
className={chatSettings.mode === mode ? 'primary-alt' : ''}
action={() => setChatSettings({...chatSettings, mode})}
>
{name}
</Button>
)

const stopBubbling = (e: any) => {
e.preventDefault()
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
}

return (
<Page noFooter>
<div className="page-body">
<div className="page-body full-height" onClick={stopBubbling}>
<Chatbot
question={question}
questions={[
'What is AI Safety?',
'How would the AI even get out in the world?',
'Do people seriously worry about existential risk from AI?',
]}
settings={chatSettings}
/>
<div className="settings-container" onClick={stopBubbling}>
{showSettings && (
<div className="settings bordered flex-container">
<div>Answer detail</div>
<ModeButton mode="default" name="Default" />
<ModeButton mode="rookie" name="Detailed" />
<ModeButton mode="concise" name="Concise" />
</div>
)}
<SettingsIcon
width="32"
height="32"
className="pointer"
onClick={() => setShowSettings((current) => !current)}
/>
</div>
</div>
</Page>
)
Expand Down
55 changes: 32 additions & 23 deletions app/routes/questions.$questionId.$.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import Error from '~/components/Error'
import XIcon from '~/components/icons-generated/X'
import ChevronRight from '~/components/icons-generated/ChevronRight'
import {ArticlesNav} from '~/components/ArticlesNav/ArticleNav'
import {fetchGlossary} from '~/routes/questions.glossary'
import {loadQuestionDetail, loadTags} from '~/server-utils/stampy'
import {QuestionStatus, loadQuestionDetail} from '~/server-utils/stampy'
import useToC from '~/hooks/useToC'
import type {Question, Glossary, Tag} from '~/server-utils/stampy'
import useGlossary from '~/hooks/useGlossary'
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'

Expand All @@ -27,11 +28,7 @@ export const loader = async ({request, params}: LoaderFunctionArgs) => {

try {
const dataPromise = loadQuestionDetail(request, questionId).catch(raise500)
const tagsPromise = loadTags(request)
.then(({data}) => data)
.catch(raise500)

return defer({question: dataPromise, tags: tagsPromise})
return defer({question: dataPromise})
} catch (error: unknown) {
console.log(error)
const msg = `No question found with ID ${questionId}. Please go to <a href="https://discord.com/invite/Bt8PaRTDQC">Discord</a> and report where you found this link.`
Expand All @@ -46,8 +43,8 @@ const dummyQuestion = (title: string | undefined) =>
tags: [],
}) as any as Question

const updateTags = (question: Question, tags: Tag[]) => {
const mappedTags = tags.reduce((acc, t) => ({...acc, [t.name]: t}), {})
const updateTags = (question: Question, tags?: Tag[]) => {
const mappedTags = tags?.reduce((acc, t) => ({...acc, [t.name]: t}), {}) || {}
return {
...question,
tags: question.tags
Expand All @@ -57,24 +54,32 @@ const updateTags = (question: Question, tags: Tag[]) => {
}
}

const updateRelated = (question: Question, allQuestions?: Question[]) => {
const live =
allQuestions
?.filter(({status}) => status === QuestionStatus.LIVE_ON_SITE)
.map(({pageid}) => pageid) || []
return {
...question,
relatedQuestions: question.relatedQuestions.filter(({pageid}) => live.includes(pageid)),
}
}

const updateFields = (question: Question, tags?: Tag[], allQuestions?: Question[]) =>
updateTags(updateRelated(question, allQuestions), tags)

export default function RenderArticle() {
const location = useLocation()
const [glossary, setGlossary] = useState<Glossary>({} as Glossary)
const [showNav, setShowNav] = useState(false) // Used on mobile
const params = useParams()
const {items: onSiteQuestions} = useOnSiteQuestions()
const {items: tags} = useTags()
const glossary = useGlossary()
const pageid = params.questionId ?? '😱'
const {question, tags} = useLoaderData<typeof loader>()
const {question} = useLoaderData<typeof loader>()
const {toc, findSection, getArticle, getPath} = useToC()
const section = findSection(location?.state?.section || pageid)

useEffect(() => {
const getGlossary = async () => {
const {data} = await fetchGlossary()
setGlossary(data)
}
getGlossary()
}, [setGlossary])

useEffect(() => {
setShowNav(false)
}, [location.key])
Expand Down Expand Up @@ -134,8 +139,8 @@ export default function RenderArticle() {
/>
}
>
<Await resolve={Promise.all([question, tags])}>
{([resolvedQuestion, resolvedTags]) => {
<Await resolve={question}>
{(resolvedQuestion) => {
if (resolvedQuestion instanceof Response || !('data' in resolvedQuestion)) {
return <Error error={resolvedQuestion} />
} else if (!resolvedQuestion.data.pageid) {
Expand All @@ -145,7 +150,11 @@ export default function RenderArticle() {
} else {
return (
<Article
question={updateTags(resolvedQuestion.data as Question, resolvedTags as Tag[])}
question={updateFields(
resolvedQuestion.data as Question,
tags,
onSiteQuestions
)}
glossary={glossary}
className={showNav ? 'desktop-only' : ''}
/>
Expand Down
Loading

0 comments on commit 37d404d

Please sign in to comment.