Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chat settings #603

Merged
merged 2 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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" />
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this icon will have to be changed, but this will suffice for styling

</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);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aw thanks. Feel free to do the minimum when it comes to CSS

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, I will :D

.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'
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all of these models should work, in case you want a faster/simpler model

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)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change this to e.g. {mode: 'default', completions: 'gpt-3.5-turbo'} if you want to use a different model

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
Loading