diff --git a/app/components/Chatbot/ChatEntry.tsx b/app/components/Chatbot/ChatEntry.tsx
new file mode 100644
index 00000000..b27ef1b0
--- /dev/null
+++ b/app/components/Chatbot/ChatEntry.tsx
@@ -0,0 +1,170 @@
+import {ComponentType} from 'react'
+import {Link} from '@remix-run/react'
+import QuestionMarkIcon from '~/components/icons-generated/QuestionMark'
+import BotIcon from '~/components/icons-generated/Bot'
+import PersonIcon from '~/components/icons-generated/Person'
+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'
+
+const hints = {
+ bot: 'bla bla bla something bot',
+ human: 'bla bla bla by humans',
+}
+
+const AnswerInfo = ({answerType}: {answerType?: 'human' | 'bot'}) => {
+ if (!answerType) return null
+ return (
+
+ {answerType === 'human' ? : }
+
+ {answerType === 'human' ? 'Human-written' : 'Bot-generated'} response
+
+
+ {hints[answerType]}
+
+ )
+}
+
+type TitleProps = {
+ title: string
+ Icon: ComponentType
+ answerType?: 'human' | 'bot'
+}
+const Title = ({title, Icon, answerType}: TitleProps) => (
+
+)
+
+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 Reference = ({id, title, authors, source, url, reference}: Citation) => {
+ const referenceSources = {
+ arxiv: 'Scientific paper',
+ blogs: 'Blogpost',
+ 'aisafety.info': 'AISafety.info',
+ }
+
+ const Authors = ({authors}: {authors?: string[]}) => {
+ if (!authors || !authors.length || authors.length === 0) return null
+ return (
+
+ {authors.slice(0, 3).join(', ')}
+ {authors.length <= 3 ? '' : ' et. al.'}
+
+ )
+ }
+
+ return (
+
+
{reference}
+
+
{title}
+
+
+
{' ยท '}
+
+ {referenceSources[source as keyof typeof referenceSources] || url}
+
+
+
+
+ )
+}
+
+const ChatbotReply = ({phase, content, citationsMap}: AssistantEntry) => {
+ const citations = [] as Citation[]
+ citationsMap?.forEach((v) => {
+ citations.push(v)
+ })
+
+ const references = citations.map(({reference}) => reference).join('')
+ const referencesRegex = new RegExp(`(\\[[${references}]\\])`)
+
+ const PhaseState = () => {
+ switch (phase) {
+ case 'started':
+ return Loading: Sending query...
+ case 'semantic':
+ return Loading: Performing semantic search...
+ case 'history':
+ return Loading: Processing history...
+ case 'context':
+ return Loading: Creating context...
+ case 'prompt':
+ return Loading: Creating prompt...
+ case 'llm':
+ return Loading: Waiting for LLM...
+ case 'streaming':
+ case 'followups':
+ default:
+ return null
+ }
+ }
+
+ return (
+
+
+
+
+ {content?.split(referencesRegex).map((chunk, i) => {
+ if (chunk.match(referencesRegex)) {
+ const ref = citationsMap?.get(chunk[1])
+ return
+ } else {
+ return {chunk}
+ }
+ })}
+
+ {citations?.map(Reference)}
+ {phase === 'followups' ?
Checking for followups...
: undefined}
+
+ )
+}
+
+const StampyArticle = ({pageid, content}: StampyEntry) => {
+ const glossary = useGlossary()
+
+ return (
+
+ )
+}
+
+const ChatEntry = (props: Entry) => {
+ const roles = {
+ user: UserQuery,
+ stampy: StampyArticle,
+ assistant: ChatbotReply,
+ } as {[k: string]: ComponentType}
+ const Role = roles[props.role] as ComponentType
+ if (!Role) return null
+ return (
+
+
+
+ )
+}
+
+export default ChatEntry
diff --git a/app/components/Chatbot/chat_entry.css b/app/components/Chatbot/chat_entry.css
new file mode 100644
index 00000000..605ba382
--- /dev/null
+++ b/app/components/Chatbot/chat_entry.css
@@ -0,0 +1,53 @@
+article.stampy {
+ background: var(--colors-light-grey);
+ padding: var(--spacing-40);
+ border-radius: var(--spacing-6);
+ max-width: unset;
+ margin: var(--spacing-16);
+ margin-left: var(--spacing-56);
+}
+
+.chat-entry .title {
+ display: flex;
+ gap: var(--spacing-16);
+}
+
+.chat-entry .info {
+ display: flex;
+ gap: var(--spacing-6);
+ align-items: center;
+}
+
+.chat-entry .hint-contents {
+ position: absolute;
+ transform: translateY(var(--spacing-24));
+ visibility: hidden;
+}
+
+.chat-entry .hint:hover + .hint-contents {
+ visibility: visible;
+ background: red;
+}
+
+.chat-entry .reference-link {
+ background: red;
+ width: var(--spacing-16);
+ height: var(--spacing-16);
+ display: inline-block;
+}
+
+.reference {
+ display: flex;
+ gap: var(--spacing-16);
+}
+.reference .reference-num {
+ width: var(--spacing-32);
+ height: var(--spacing-32);
+ background: red;
+ border-radius: 6px;
+ text-align: center;
+}
+
+.reference .source-link {
+ color: var(--colors-teal-500);
+}
diff --git a/app/components/Chatbot/index.tsx b/app/components/Chatbot/index.tsx
index b3267174..8dd8292d 100644
--- a/app/components/Chatbot/index.tsx
+++ b/app/components/Chatbot/index.tsx
@@ -1,11 +1,13 @@
-import {useState} from 'react'
-import {Link} from '@remix-run/react'
-import PersonIcon from '~/components/icons-generated/Person'
+import {useEffect, useState} from 'react'
+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, Followup} from '~/hooks/useChat'
+import {queryLLM, Entry, AssistantEntry, StampyEntry, Followup} from '~/hooks/useChat'
+import ChatEntry from './ChatEntry'
import './widgit.css'
+import {questionUrl} from '~/routesMapper'
+import {Question} from '~/server-utils/stampy'
export const WidgetStampy = () => {
const [question, setQuestion] = useState('')
@@ -93,14 +95,15 @@ const QuestionInput = ({initial, onChange, onAsk}: QuestionInputProps) => {
type FollowupsProps = {
title?: string
followups?: Followup[]
+ onSelect: (followup: Followup) => void
}
-const Followups = ({title, followups}: FollowupsProps) => (
+const Followups = ({title, followups, onSelect}: FollowupsProps) => (
<>
{title && {title}
}
- {followups?.map(({text, action}, i) => (
+ {followups?.map(({text, pageid}, i) => (
-
@@ -123,77 +126,58 @@ const SplashScreen = ({
({text, action: () => onQuestion(text)}))}
+ followups={questions?.map((text: string) => ({text}))}
+ onSelect={({text}: Followup) => onQuestion(text)}
/>
>
)
-const UserQuery = ({content}: Entry) => (
-
-)
-
-const ChatbotReply = ({content, phase}: AssistantEntry) => {
- const PhaseState = () => {
- switch (phase) {
- case 'started':
- return Loading: Sending query...
- case 'semantic':
- return Loading: Performing semantic search...
- case 'history':
- return Loading: Processing history...
- case 'context':
- return Loading: Creating context...
- case 'prompt':
- return Loading: Creating prompt...
- case 'llm':
- return Loading: Waiting for LLM...
- case 'streaming':
- case 'followups':
- default:
- return null
- }
- }
-
- return (
-
-
- Stampy
-
-
-
{content}
- {phase === 'followups' ?
Checking for followups...
: undefined}
-
- )
-}
-
-const ChatEntry = (props: Entry) => {
- switch (props.role) {
- case 'user':
- return
- case 'assistant':
- return
- }
-}
-
export const Chatbot = ({question, questions}: {question?: string; questions?: string[]}) => {
const [followups, setFollowups] = useState()
const [sessionId] = useState('asd')
const [history, setHistory] = useState([] as Entry[])
const [controller, setController] = useState(() => new AbortController())
-
- const showFollowup = (pageid: string) => {
- // Fetch and display the given article
- console.log(pageid)
+ const fetcher = useFetcher({key: 'followup-fetcher'})
+
+ useEffect(() => {
+ if (!fetcher.data || fetcher.state !== 'idle') return
+
+ const question = (fetcher.data as any)?.question?.data as Question
+ if (!question || !question.pageid) return
+
+ setHistory((history) =>
+ history.map((item, i) => {
+ // Ignore non human written entries
+ if ((item as StampyEntry).pageid !== question.pageid) return item
+ // this is the current entry, so update it
+ if (i === history.length - 1) {
+ setFollowups(
+ question.relatedQuestions?.slice(0, 3).map(({title, pageid}) => ({text: title, pageid}))
+ )
+ return {...item, content: question.text || ''}
+ }
+ // this is a previous human written article that didn't load properly - don't
+ // update the text as that could cause things to jump around - the user has
+ // already moved on, anyway
+ if (!item.content) return {...item, content: '...'}
+ // Any fully loaded previous human articles should just be returned
+ return item
+ })
+ )
+ }, [fetcher.data, fetcher.state])
+
+ const showFollowup = async ({text, pageid}: Followup) => {
+ if (pageid) fetcher.load(questionUrl({pageid}))
+ setHistory((prev) => [
+ ...prev,
+ {role: 'user', content: text},
+ {pageid, role: 'stampy'} as StampyEntry,
+ ])
+ setFollowups(undefined)
}
- const abortSearch = () => controller.abort()
- console.log(abortSearch) // to stop the linter from complaining
+ const abortSearch = () => controller.abort() // eslint-disable-line @typescript-eslint/no-unused-vars
const onQuestion = async (question: string) => {
const message = {content: question, role: 'user'} as Entry
@@ -212,12 +196,7 @@ export const Chatbot = ({question, questions}: {question?: string; questions?: s
controller
)
updateReply(result)
- setFollowups(
- followups?.map(({text, pageid}: Followup) => ({
- text,
- action: () => pageid && showFollowup(pageid),
- }))
- )
+ setFollowups(followups)
}
return (
@@ -229,7 +208,11 @@ export const Chatbot = ({question, questions}: {question?: string; questions?: s
))}
{followups ? (
-
+
) : undefined}
diff --git a/app/hooks/useChat.ts b/app/hooks/useChat.ts
index 234a2090..c99f54ec 100644
--- a/app/hooks/useChat.ts
+++ b/app/hooks/useChat.ts
@@ -5,11 +5,14 @@ export type Citation = {
authors: string[]
date: string
url: string
+ source: string
index: number
text: string
+ reference: string
+ id?: string
}
-export type Entry = UserEntry | AssistantEntry | ErrorMessage | StampyMessage
+export type Entry = UserEntry | AssistantEntry | ErrorMessage | StampyEntry
export type ChatPhase =
| 'started'
| 'semantic'
@@ -41,14 +44,17 @@ export type ErrorMessage = {
deleted?: boolean
}
-export type StampyMessage = {
+export type StampyEntry = {
role: 'stampy'
+ pageid: string
content: string
- url: string
deleted?: boolean
}
-export type Followup = {text: string; pageid?: string; action: string | (() => void)}
+export type Followup = {
+ text: string
+ pageid?: string
+}
export type SearchResult = {
followups?: Followup[]
result: Entry
diff --git a/app/hooks/useGlossary.ts b/app/hooks/useGlossary.ts
index e69de29b..e35d4b8c 100644
--- a/app/hooks/useGlossary.ts
+++ b/app/hooks/useGlossary.ts
@@ -0,0 +1,8 @@
+import {useGlossary as useCachedGlossary} from '~/hooks/useCachedObjects'
+
+const useGlossary = () => {
+ const {items} = useCachedGlossary()
+ return items
+}
+
+export default useGlossary
diff --git a/app/newRoot.css b/app/root.css
similarity index 99%
rename from app/newRoot.css
rename to app/root.css
index d55d4540..cd8548f8 100644
--- a/app/newRoot.css
+++ b/app/root.css
@@ -12,6 +12,7 @@
--colors-cool-grey-300: #c7cdd5;
--colors-cool-grey-200: #dfe3e9;
--colors-cool-grey-100: #f9fafc;
+ --colors-light-grey: #f9fafc;
--colors-white: #ffffff;
--colors-teal-900: #115652;
diff --git a/app/root.tsx b/app/root.tsx
index b1bca492..53cf2118 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -12,7 +12,7 @@ import {
} from '@remix-run/react'
import type {MetaFunction, LinksFunction, LoaderFunction} from '@remix-run/cloudflare'
import {cssBundleHref} from '@remix-run/css-bundle'
-import newStyles from '~/newRoot.css'
+import newStyles from '~/root.css'
import Error from '~/components/Error'
import Page from '~/components/Page'
import {CachedObjectsProvider} from '~/hooks/useCachedObjects'
diff --git a/app/routes/questions.$questionId.$.tsx b/app/routes/questions.$questionId.$.tsx
index b2af8c04..a3862bf7 100644
--- a/app/routes/questions.$questionId.$.tsx
+++ b/app/routes/questions.$questionId.$.tsx
@@ -30,8 +30,10 @@ export const loader = async ({request, params}: LoaderFunctionArgs) => {
const tagsPromise = loadTags(request)
.then(({data}) => data)
.catch(raise500)
+
return defer({question: dataPromise, tags: tagsPromise})
} catch (error: unknown) {
+ console.log(error)
const msg = `No question found with ID ${questionId}. Please go to Discord and report where you found this link.`
throw new Response(msg, {status: 404})
}