diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml
index 8d51c6c1..a7634beb 100644
--- a/.github/workflows/deploy-dev.yml
+++ b/.github/workflows/deploy-dev.yml
@@ -24,7 +24,7 @@ jobs:
| sed s/{CODA_TOKEN}/${{ secrets.CODA_TOKEN }}/ \
| sed s/{CODA_INCOMING_TOKEN}/${{ secrets.CODA_INCOMING_TOKEN }}/ \
| sed s/{CODA_WRITES_TOKEN}/${{ secrets.CODA_WRITES_TOKEN }}/ \
- | sed s/{GOOGLE_ANALYTICS_ID}/${{ secrets.GOOGLE_ANALYTICS_ID }}/ \
+ | sed s/{MATOMO_DOMAIN}/${{ secrets.MATOMO_DOMAIN }}/ \
| sed s/{DISCORD_LOGGING_CHANNEL_ID}/${{ secrets.DISCORD_LOGGING_CHANNEL_ID }}/ \
| sed s/{DISCORD_LOGGING_TOKEN}/${{ secrets.DISCORD_LOGGING_TOKEN }}/ \
> wrangler.toml
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index a9031a90..0bca1d15 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -23,7 +23,7 @@ jobs:
| sed s/{CODA_TOKEN}/${{ secrets.CODA_TOKEN }}/ \
| sed s/{CODA_INCOMING_TOKEN}/${{ secrets.CODA_INCOMING_TOKEN }}/ \
| sed s/{CODA_WRITES_TOKEN}/${{ secrets.CODA_WRITES_TOKEN }}/ \
- | sed s/{GOOGLE_ANALYTICS_ID}/${{ secrets.GOOGLE_ANALYTICS_ID }}/ \
+ | sed s/{MATOMO_DOMAIN}/${{ secrets.MATOMO_DOMAIN }}/ \
| sed s/{DISCORD_LOGGING_CHANNEL_ID}/${{ secrets.DISCORD_LOGGING_CHANNEL_ID }}/ \
| sed s/{DISCORD_LOGGING_TOKEN}/${{ secrets.DISCORD_LOGGING_TOKEN }}/ \
> wrangler.toml
diff --git a/app/components/Article/article.css b/app/components/Article/article.css
index 10d06fd4..63dc3960 100644
--- a/app/components/Article/article.css
+++ b/app/components/Article/article.css
@@ -175,6 +175,21 @@ article a.see-more.visible:after {
content: 'See less';
}
+article .banner {
+ padding: var(--spacing-12) var(--spacing-24);
+ margin-bottom: var(--spacing-12);
+}
+article .banner h3 {
+ display: flex;
+ align-items: center;
+}
+article .banner h3 img {
+ width: 2em;
+}
+article .banner h3 .title {
+ padding-left: 10px;
+}
+
@media only screen and (max-width: 780px) {
article {
max-width: 100%;
diff --git a/app/components/Article/index.tsx b/app/components/Article/index.tsx
index 905f51d7..7ddc29f6 100644
--- a/app/components/Article/index.tsx
+++ b/app/components/Article/index.tsx
@@ -5,8 +5,8 @@ import CopyIcon from '~/components/icons-generated/Copy'
import EditIcon from '~/components/icons-generated/Pencil'
import Button, {CompositeButton} from '~/components/Button'
import Feedback from '~/components/Feedback'
-import type {Glossary, Question} from '~/server-utils/stampy'
import {tagUrl} from '~/routesMapper'
+import type {Glossary, Question, Banner as BannerType} from '~/server-utils/stampy'
import Contents from './Contents'
import './article.css'
@@ -87,6 +87,30 @@ const ArticleMeta = ({question, className}: {question: Question; className?: str
)
}
+const Banner = ({title, text, icon, backgroundColour, textColour}: BannerType) => {
+ return (
+
+
+
+ {title}
+
+
+
+ )
+}
+
const Tags = ({tags}: Question) => (
{tags?.map((tag) => (
@@ -108,6 +132,7 @@ export const Article = ({question, glossary, className}: ArticleProps) => {
return (
{title}
+ {question.banners?.map(Banner)}
diff --git a/app/components/Chatbot/index.tsx b/app/components/Chatbot/index.tsx
index d099d446..ffdd6850 100644
--- a/app/components/Chatbot/index.tsx
+++ b/app/components/Chatbot/index.tsx
@@ -170,7 +170,7 @@ const SplashScreen = ({
- n
Hi there, I'm Stampy.
+ Hi there, I'm Stampy.
I can answer your questions about AI Safety
{
const [followups, setFollowups] = useState()
// FIXME: Generate session id
- const [sessionId] = useState('asd')
+ const [sessionId] = useState(crypto.randomUUID())
const [history, setHistory] = useState([] as Entry[])
const [controller, setController] = useState(() => new AbortController())
const fetcher = useFetcher({key: 'followup-fetcher'})
diff --git a/app/hooks/useQuestionStateInUrl.ts b/app/hooks/useQuestionStateInUrl.ts
deleted file mode 100644
index d84e6858..00000000
--- a/app/hooks/useQuestionStateInUrl.ts
+++ /dev/null
@@ -1,292 +0,0 @@
-import {useState, useRef, useEffect, useMemo, useCallback} from 'react'
-import {useSearchParams, useNavigation} from '@remix-run/react'
-import {Question, QuestionState, RelatedQuestion, PageId, Glossary} from '~/server-utils/stampy'
-import {fetchAllQuestionsOnSite} from '~/routes/questions.allQuestionsOnSite'
-import {fetchGlossary} from '~/routes/questions.glossary'
-import {
- processStateEntries,
- getStateEntries,
- addQuestions as addQuestionsToState,
- insertInto as insertIntoState,
- moveQuestion as moveQuestionInState,
- moveToTop as moveQuestionToTop,
-} from '~/hooks/stateModifiers'
-
-function updateQuestionMap(question: Question, map: Map): Map {
- map.set(question.pageid, question)
- for (const {pageid, title} of question.relatedQuestions) {
- if (!pageid || map.has(pageid)) continue
-
- map.set(pageid, {
- title,
- pageid,
- text: null,
- answerEditLink: null,
- relatedQuestions: [],
- tags: [],
- banners: [],
- })
- }
- return map
-}
-
-const emptyQuestionArray: Question[] = []
-
-export default function useQuestionStateInUrl(initialQuestions: Question[]) {
- const [remixSearchParams, setRemixParams] = useSearchParams()
- const transition = useNavigation()
- const embedWithoutDetails =
- remixSearchParams.has('embed') && !remixSearchParams.has('showDetails')
- const queryFromUrl = remixSearchParams.get('q') || ''
- const limitFromUrl = parseInt(remixSearchParams.get('limit') ?? '', 10) || undefined
-
- const removeQueryFromUrl = useCallback(() => {
- remixSearchParams.delete('q')
- setRemixParams(remixSearchParams)
- }, [remixSearchParams, setRemixParams])
-
- const [stateString, setStateString] = useState(
- () =>
- remixSearchParams.get('state') && processStateEntries(remixSearchParams.get('state') ?? '')
- )
- const [questionMap, setQuestionMap] = useState(() => {
- const initialMap: Map = new Map()
- for (const question of initialQuestions) {
- updateQuestionMap(question, initialMap)
- }
- return initialMap
- })
- const [glossary, setGlossary] = useState({} as Glossary)
-
- const onSiteQuestionsRef = useRef(emptyQuestionArray)
- const allowBrowserBackToInitialStateRef = useRef(true)
-
- useEffect(() => {
- // not needed for initial screen => lazy load on client
- fetchAllQuestionsOnSite().then(({data, backgroundPromiseIfReloaded}) => {
- onSiteQuestionsRef.current = data
- backgroundPromiseIfReloaded.then((x) => {
- if (x) onSiteQuestionsRef.current = x.data
- })
- })
- fetchGlossary().then(({data, backgroundPromiseIfReloaded}) => {
- setGlossary(data)
- backgroundPromiseIfReloaded.then((x) => {
- if (x) setGlossary(x.data)
- })
- })
- }, [])
-
- useEffect(() => {
- if (transition.location) {
- const state = new URLSearchParams(transition.location.search).get('state')
- setStateString(state)
- }
- }, [transition.location])
-
- const initialCollapsedState = useMemo(
- () => initialQuestions.map(({pageid}) => `${pageid}${QuestionState.COLLAPSED}`).join(''),
- [initialQuestions]
- )
- const questions: Question[] = useMemo(() => {
- return getStateEntries(stateString ?? initialCollapsedState).map(([pageid, questionState]) => ({
- pageid,
- title: '...',
- text: null,
- answerEditLink: null,
- relatedQuestions: [],
- questionState,
- tags: [],
- banners: [],
- ...questionMap.get(pageid),
- }))
- }, [stateString, initialCollapsedState, questionMap])
-
- const moveToTop = (currentState: string, {pageid}: Question) => {
- setTimeout(() => {
- // scroll to top after the state is updated
- window.scrollTo({
- top: 0,
- behavior: 'smooth',
- })
- })
- return moveQuestionToTop(currentState, pageid)
- }
-
- /*
- * Get all related questions of the provided `question` that aren't already displayed on the site
- */
- const unshownRelatedQuestions = (
- questions: Question[],
- questionProps: Question
- ): RelatedQuestion[] => {
- const {relatedQuestions} = questionProps
-
- const onSiteQuestions = onSiteQuestionsRef.current
- const onSiteSet = new Set(onSiteQuestions.map(({pageid}) => pageid))
-
- return relatedQuestions.filter((question) => {
- const isOnSite = onSiteSet.has(question.pageid)
- // hide already displayed questions, detect duplicates by pageid (pageid can be different due to redirects)
- // TODO: #25 relocate already displayed to slide in as a new related one
- const isAlreadyDisplayed = questions.some(({pageid}) => pageid === question.pageid)
- return isOnSite && !isAlreadyDisplayed
- })
- }
-
- /*
- * Update the window.location with the new URL state
- */
- const updateStateString = useCallback(
- (newState: string) => {
- if (stateString == newState) return
- const newSearchParams = new URLSearchParams(remixSearchParams)
- newSearchParams.set('state', newState)
-
- const newUrl = '?' + newSearchParams.toString()
- if (allowBrowserBackToInitialStateRef.current) {
- history.pushState(newState, '', newUrl)
- allowBrowserBackToInitialStateRef.current = false
- } else {
- history.replaceState(newState, '', newUrl)
- }
-
- setStateString(newState)
- },
- [remixSearchParams, stateString]
- )
-
- /*
- * Add the given `questions` to the global questions map. This will not update the URL
- */
- const mergeNewQuestions = useCallback((questions: Question[]) => {
- setQuestionMap((currentMap) =>
- questions.reduce((map, question) => updateQuestionMap(question, map), new Map(currentMap))
- )
- }, [])
-
- /*
- * Add the given `newQuestions` to the collection of questions. This will also update the URL
- */
- const addQuestions = useCallback(
- (newQuestions: Question[]) => {
- const questions = newQuestions.filter((q) => !questionMap.get(q.pageid))
- mergeNewQuestions(questions)
-
- const newState = addQuestionsToState(stateString ?? initialCollapsedState, questions)
- updateStateString(newState)
- },
- [initialCollapsedState, stateString, questionMap, updateStateString, mergeNewQuestions]
- )
-
- /*
- * Toggle the selected question.
- *
- * If the question is to be opened, this will also make sure all it's related questions
- * are on the page
- */
- const toggleQuestion = useCallback(
- (questionProps: Question, options?: {moveToTop?: boolean; onlyRelated?: boolean}) => {
- const {pageid, relatedQuestions} = questionProps
-
- let currentState = stateString ?? initialCollapsedState
-
- if (options?.moveToTop) {
- currentState = moveToTop(currentState, questionProps)
- }
-
- const onSiteQuestions = onSiteQuestionsRef.current
- if (onSiteQuestions.length === 0 && relatedQuestions.length > 0) {
- // if onSiteAnswers (needed for relatedQuestions) are not loaded yet, wait a moment to re-run
- setTimeout(() => toggleQuestion(questionProps, options), 200)
- return
- }
-
- const newRelatedQuestions = unshownRelatedQuestions(questions, questionProps)
- const newState = insertIntoState(currentState, pageid, newRelatedQuestions, {
- toggle: !options?.onlyRelated,
- })
-
- updateStateString(newState)
- },
- [initialCollapsedState, questions, stateString, updateStateString]
- )
-
- const onLazyLoadQuestion = useCallback(
- (question: Question) => mergeNewQuestions([question]),
- [mergeNewQuestions]
- )
-
- const moveQuestion = useCallback(
- (pageId: PageId, to: PageId | null) => {
- const currentState = stateString ?? initialCollapsedState
- updateStateString(moveQuestionInState(currentState, pageId, to ?? ''))
- },
- [initialCollapsedState, stateString, updateStateString]
- )
-
- /*
- * Moves the given question to the top of the page, opens it, and make sure all related ones are loaded
- */
- const selectQuestion = useCallback(
- (pageid: string, title: string) => {
- // if the question is already loaded, move it to top
- for (const q of questionMap.values()) {
- if (pageid === q.pageid) {
- toggleQuestion(q, {moveToTop: true})
- return
- }
- }
- // else show the new question in main view and let the Question component fetch it
- const tmpQuestion: Question = {
- pageid,
- title,
- text: null,
- answerEditLink: null,
- relatedQuestions: [],
- tags: [],
- banners: [],
- }
- onLazyLoadQuestion(tmpQuestion)
- toggleQuestion(tmpQuestion, {moveToTop: true})
- },
- [onLazyLoadQuestion, questionMap, toggleQuestion]
- )
-
- // if there is only 1 question from a direct link, load related questions too
- useEffect(() => {
- if (questions.length === 1) {
- const insertAfterOnSiteStatusIsKnown = () => {
- if (onSiteQuestionsRef.current.length === 0) {
- setTimeout(insertAfterOnSiteStatusIsKnown, 200)
- return
- }
- const relatedQuestions = unshownRelatedQuestions([], questions[0])
- const newState = insertIntoState(
- stateString ?? initialCollapsedState,
- questions[0].pageid,
- relatedQuestions,
- {toggle: false}
- )
-
- updateStateString(newState)
- }
- insertAfterOnSiteStatusIsKnown()
- }
- }, [questions, stateString, initialCollapsedState, updateStateString])
-
- return {
- questions,
- onSiteQuestionsRef,
- toggleQuestion,
- onLazyLoadQuestion,
- selectQuestion,
- addQuestions,
- moveQuestion,
- glossary,
- embedWithoutDetails,
- queryFromUrl,
- limitFromUrl,
- removeQueryFromUrl,
- }
-}
diff --git a/app/hooks/useSearch.tsx b/app/hooks/useSearch.tsx
index e3063154..d363e4b0 100644
--- a/app/hooks/useSearch.tsx
+++ b/app/hooks/useSearch.tsx
@@ -1,6 +1,6 @@
import {useState, useEffect, useRef} from 'react'
-import {fetchAllQuestionsOnSite} from '~/routes/questions.allQuestionsOnSite'
import {Question} from '~/server-utils/stampy'
+import {useOnSiteQuestions} from './useCachedObjects'
const NUM_RESULTS = 8
@@ -140,16 +140,7 @@ export const useSearch = (numResults = NUM_RESULTS) => {
const resultsForRef = useRef()
const [results, setResults] = useState([] as SearchResult[])
- const onSiteAnswersRef = useRef([])
- 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 {items} = useOnSiteQuestions()
useEffect(() => {
const makeWorker = async () => {
@@ -183,7 +174,7 @@ export const useSearch = (numResults = NUM_RESULTS) => {
const search = (userQuery: string, minSimilarity?: number) => {
isPendingSearch.current = true
const wordCount = userQuery.split(' ').length
- if (wordCount > 2) {
+ if (wordCount > 2 && tfWorkerRef.current) {
if (runningQueryRef.current || !tfWorkerRef.current) {
searchLater(userQuery, minSimilarity)
return
@@ -191,20 +182,18 @@ export const useSearch = (numResults = NUM_RESULTS) => {
runningQueryRef.current = userQuery
tfWorkerRef.current.postMessage({userQuery, numResults, minSimilarity})
} else {
- if (runningQueryRef.current || onSiteAnswersRef.current.length == 0) {
+ if (!items || items?.length == 0) {
searchLater(userQuery, minSimilarity)
return
}
runningQueryRef.current = userQuery
- baselineSearch(userQuery, onSiteAnswersRef.current, minSimilarity, numResults).then(
- (searchResults) => {
- runningQueryRef.current = undefined
- resultsRef.current = searchResults
- resultsForRef.current = userQuery
- isPendingSearch.current = false
- setResults(searchResults)
- }
- )
+ baselineSearch(userQuery, items, minSimilarity, numResults).then((searchResults) => {
+ runningQueryRef.current = undefined
+ resultsRef.current = searchResults
+ resultsForRef.current = userQuery
+ isPendingSearch.current = false
+ setResults(searchResults)
+ })
}
}
diff --git a/app/root.tsx b/app/root.tsx
index 14b8c447..e1300672 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -101,31 +101,31 @@ export const loader = async ({request}: Parameters[0]) => {
url: request.url,
embed,
showSearch,
- gaTrackingId: GOOGLE_ANALYTICS_ID,
+ matomoDomain: MATOMO_DOMAIN,
}
}
-const GoogleAnalytics = ({gaTrackingId}: {gaTrackingId?: string}) => {
- if (!gaTrackingId) return null
+const AnaliticsTag = ({matomoDomain}: {matomoDomain?: string}) => {
+ if (!matomoDomain) return null
return (
- <>
-
-
- >
+
)
}
@@ -182,7 +182,7 @@ type Loader = Awaited>
export type Context = Pick
export default function App() {
- const {embed, showSearch, gaTrackingId} = useLoaderData()
+ const {embed, showSearch, matomoDomain} = useLoaderData()
const {savedTheme} = useTheme()
const context: Context = {embed, showSearch}
@@ -208,7 +208,7 @@ export default function App() {
return (
-
+
diff --git a/app/routes/questions.editors.tsx b/app/routes/questions.editors.tsx
index 465f7a6a..e69de29b 100644
--- a/app/routes/questions.editors.tsx
+++ b/app/routes/questions.editors.tsx
@@ -1,77 +0,0 @@
-import Nav from '~/components/Nav'
-import Footer from '~/components/Footer'
-import {fetchAllQuestionsOnSite} from '~/routes/questions.allQuestionsOnSite'
-import type {Question} from '../server-utils/stampy'
-
-const formatTwineQuestion = ({title, text, relatedQuestions, tags}: Question) => {
- const formattedTags = tags.map((t) => t.replaceAll(' ', '-')).join(' ')
- const links = relatedQuestions.map(({title}) => `[[${title}]]`).join('\n')
-
- return `:: ${title} [${formattedTags}] {"size":"100,100"}\n${text}\n${links}`
-}
-
-const generateTwine = (data: Question[]) => {
- const first = data.filter(({pageid}) => pageid == '9OGZ')[0]
-
- return (
- `:: StoryTitle
-Stampy
-
-
-:: StoryData
-{
-"ifid": "3B314495-7266-46FE-AD5E-96C9F6FD54D4",
-"format": "Snowman",
-"format-version": "2.0.2",
-"start": "${first.title}",
-"zoom": 1
-}
- ` + data.map(formatTwineQuestion).join('\n\n')
- )
-}
-
-export default function App() {
- const downloadTwine = async () => {
- const {data} = await fetchAllQuestionsOnSite()
-
- const blob = new Blob([generateTwine(data)], {type: 'text/plain'})
-
- // Create a temporary URL for the blob
- const url = URL.createObjectURL(blob)
-
- // Create a temporary "download" link
- const link = document.createElement('a')
- link.href = url
- link.download = 'stampy.twee'
-
- // Programatically triggering the download
- link.click()
-
- // Cleanup: removing the temporary URL object
- URL.revokeObjectURL(url)
- }
-
- return (
- <>
-
-
-
-
-
-
- >
- )
-}
diff --git a/app/routes/questions.old.$questionId.tsx b/app/routes/questions.old.$questionId.tsx
deleted file mode 100644
index e69de29b..00000000
diff --git a/app/routes/questions/log.tsx b/app/routes/questions/log.tsx
deleted file mode 100644
index 05407a16..00000000
--- a/app/routes/questions/log.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import type {ActionFunction} from '@remix-run/cloudflare'
-
-export const action = async ({request}: Parameters[0]) => {
- const {query, name, type} = (await request.json()) as any
- const url = `${NLP_SEARCH_ENDPOINT}/api/log_query?name=${name}&type=${type}&query=${query}`
- return await fetch(url)
-}
diff --git a/remix.env.d.ts b/remix.env.d.ts
index d6850c57..f478e17f 100644
--- a/remix.env.d.ts
+++ b/remix.env.d.ts
@@ -10,6 +10,6 @@ declare const CODA_WRITES_TOKEN: string
declare const NLP_SEARCH_ENDPOINT: string
declare const ALLOW_ORIGINS: string
declare const CHATBOT_URL: string
-declare const GOOGLE_ANALYTICS_ID: string
+declare const MATOMO_DOMAIN: string
declare const DISCORD_LOGGING_CHANNEL_ID: string
declare const DISCORD_LOGGING_TOKEN: string
diff --git a/wrangler.toml.template b/wrangler.toml.template
index 1ff813c1..1eb26e82 100644
--- a/wrangler.toml.template
+++ b/wrangler.toml.template
@@ -21,6 +21,6 @@ CODA_INCOMING_TOKEN = "{CODA_INCOMING_TOKEN}"
CODA_WRITES_TOKEN = "{CODA_WRITES_TOKEN}"
NLP_SEARCH_ENDPOINT = "https://stampy-nlp-t6p37v2uia-uw.a.run.app/"
ALLOW_ORIGINS = "https://chat.aisafety.info"
-GOOGLE_ANALYTICS_ID = "{GOOGLE_ANALYTICS_ID}"
+MATOMO_DOMAIN = "{MATOMO_DOMAIN}"
DISCORD_LOGGING_CHANNEL_ID = "{DISCORD_LOGGING_CHANNEL_ID}"
-DISCORD_LOGGING_TOKEN = "{DISCORD_LOGGING_TOKEN}"
\ No newline at end of file
+DISCORD_LOGGING_TOKEN = "{DISCORD_LOGGING_TOKEN}"