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 ( +
+

+ {icon?.name} + {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 ( - <> -