From b0dfd03d71db693f919f1dabbc56874a7f66afe5 Mon Sep 17 00:00:00 2001 From: Daniel O'Connell Date: Tue, 13 Feb 2024 16:58:46 +0100 Subject: [PATCH] connect keep reading --- app/components/Article/KeepGoing/index.tsx | 40 ++++++ .../KeepGoing}/keepGoing.css | 0 app/components/Article/index.tsx | 5 +- app/components/ArticleKeepGoing/index.tsx | 47 ------- app/components/Table/index.tsx | 7 +- app/hooks/stateModifiers.tsx | 4 +- app/hooks/useCachedObjects.tsx | 3 +- app/hooks/useQuestionStateInUrl.ts | 4 +- app/hooks/useToC.tsx | 23 ++++ app/routes/questions.add.tsx | 6 +- app/routes/tags.single.$tag.tsx | 4 +- app/server-utils/stampy.ts | 10 +- stories/ArticlesKeepGoing.stories.tsx | 124 ++++++++++++++---- 13 files changed, 189 insertions(+), 88 deletions(-) create mode 100644 app/components/Article/KeepGoing/index.tsx rename app/components/{ArticleKeepGoing => Article/KeepGoing}/keepGoing.css (100%) delete mode 100644 app/components/ArticleKeepGoing/index.tsx diff --git a/app/components/Article/KeepGoing/index.tsx b/app/components/Article/KeepGoing/index.tsx new file mode 100644 index 00000000..0f7c72da --- /dev/null +++ b/app/components/Article/KeepGoing/index.tsx @@ -0,0 +1,40 @@ +import Button from '~/components/Button' +import ListTable from '~/components/Table' +import {ArrowRight} from '~/components/icons-generated' +import useToC from '~/hooks/useToC' +import type {TOCItem} from '~/routes/questions.toc' +import type {Question} from '~/server-utils/stampy' +import './keepGoing.css' + +const NextArticle = ({category, next}: {category?: string; next?: TOCItem}) => + next && ( + <> +

Keep going! 👉

+ Continue with the next article in {category} +
+ {next.title} + +
+ + ) + +export const KeepGoing = ({pageid, relatedQuestions}: Question) => { + const {findSection, getNext} = useToC() + const {category} = findSection(pageid) || {} + const next = getNext(pageid) + const hasRelated = relatedQuestions && relatedQuestions.length > 0 + + return ( +
+ + + {next && hasRelated && Or jump to a related question} + {hasRelated && ({...i, hasIcon: true}))} />} +
+ ) +} + +export default KeepGoing diff --git a/app/components/ArticleKeepGoing/keepGoing.css b/app/components/Article/KeepGoing/keepGoing.css similarity index 100% rename from app/components/ArticleKeepGoing/keepGoing.css rename to app/components/Article/KeepGoing/keepGoing.css diff --git a/app/components/Article/index.tsx b/app/components/Article/index.tsx index a96dca06..c5624c49 100644 --- a/app/components/Article/index.tsx +++ b/app/components/Article/index.tsx @@ -1,4 +1,5 @@ import {useRef, useState, useEffect} from 'react' +import KeepGoing from '~/components/Article/KeepGoing' import CopyIcon from '~/components/icons-generated/Copy' import EditIcon from '~/components/icons-generated/Pencil' import ThumbUpIcon from '~/components/icons-generated/ThumbUp' @@ -232,7 +233,7 @@ export const Article = ({question, glossary}: ArticleProps) => { const time = text.split(' ') return Math.ceil(time.length / rate) // ceil to avoid "0 min read" } - console.log('Question', question) + return (

{title}

@@ -244,6 +245,8 @@ export const Article = ({question, glossary}: ArticleProps) => { + +
diff --git a/app/components/ArticleKeepGoing/index.tsx b/app/components/ArticleKeepGoing/index.tsx deleted file mode 100644 index 6129fa77..00000000 --- a/app/components/ArticleKeepGoing/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React, {ReactNode} from 'react' -import Button from '../Button' -import ListTable from '~/components/Table' -import './keepGoing.css' -export interface Article { - title: string - pageid: string -} -export interface NextArticle { - title: string - pageid: string - icon: any -} -export interface ArticleKeepGoingProps { - /** - * Category of the article - */ - category: string - /** - * Related articles - */ - articles: Article[] - /** - * Next article - */ - next: NextArticle -} -export const ArticleKeepGoing = ({category, articles, next}: ArticleKeepGoingProps) => { - const nextArticle = (pageId) => { - console.log('Next article') - location.href = `/${pageId}` - } - return ( -
-

Keep going! 👉

- Continue with the next article in {category} -
- {next.title} - -
- Or, jump to related questions - -
- ) -} diff --git a/app/components/Table/index.tsx b/app/components/Table/index.tsx index e4231cf3..9b3f65d0 100644 --- a/app/components/Table/index.tsx +++ b/app/components/Table/index.tsx @@ -2,11 +2,16 @@ import {Link} from '@remix-run/react' import {ArrowUpRight} from '~/components/icons-generated' import './listTable.css' +export type ListItem = { + pageid: string + title: string + hasIcon?: boolean +} export type ListTableProps = { /** * Browse by category */ - elements: any[] + elements: ListItem[] } export const ListTable = ({elements}: ListTableProps) => ( diff --git a/app/hooks/stateModifiers.tsx b/app/hooks/stateModifiers.tsx index 4bc3909f..5a3fe5d7 100644 --- a/app/hooks/stateModifiers.tsx +++ b/app/hooks/stateModifiers.tsx @@ -1,4 +1,4 @@ -import {Question, QuestionState, RelatedQuestions, PageId} from '~/server-utils/stampy' +import {Question, QuestionState, RelatedQuestion, PageId} from '~/server-utils/stampy' type StateEntry = [PageId, QuestionState] type StateString = string @@ -70,7 +70,7 @@ export const insertAfter = (state: StateString, pageId: PageId, to: PageId): Sta export const insertInto = ( state: StateString, pageid: PageId, - relatedQuestions: RelatedQuestions, + relatedQuestions: RelatedQuestion[], options = {toggle: true} ): StateString => processStateEntries(state, (entries: StateEntry[]) => diff --git a/app/hooks/useCachedObjects.tsx b/app/hooks/useCachedObjects.tsx index 1a6d373c..6f3b5f6a 100644 --- a/app/hooks/useCachedObjects.tsx +++ b/app/hooks/useCachedObjects.tsx @@ -29,7 +29,7 @@ type useCachedObjectsType = { tags: useObjectsType toc: useObjectsType } -const CachedObjectsContext = createContext(null) +export const CachedObjectsContext = createContext(null) const getGlossary = async () => (await fetchGlossary()).data const getTags = async () => (await fetchTags()).tags @@ -40,7 +40,6 @@ export const CachedObjectsProvider = ({children}: {children: ReactElement}) => { const tags = useItemsFuncs(getTags) const toc = useItemsFuncs(getToC) - console.log('caching') return ( {children} diff --git a/app/hooks/useQuestionStateInUrl.ts b/app/hooks/useQuestionStateInUrl.ts index 57d6fa1f..a3238b4a 100644 --- a/app/hooks/useQuestionStateInUrl.ts +++ b/app/hooks/useQuestionStateInUrl.ts @@ -1,6 +1,6 @@ import {useState, useRef, useEffect, useMemo, useCallback} from 'react' import {useSearchParams, useNavigation} from '@remix-run/react' -import {Question, QuestionState, RelatedQuestions, PageId, Glossary} from '~/server-utils/stampy' +import {Question, QuestionState, RelatedQuestion, PageId, Glossary} from '~/server-utils/stampy' import {fetchAllQuestionsOnSite} from '~/routes/questions.allQuestionsOnSite' import {fetchGlossary} from '~/routes/questions.glossary' import { @@ -136,7 +136,7 @@ export default function useQuestionStateInUrl(minLogo: boolean, initialQuestions const unshownRelatedQuestions = ( questions: Question[], questionProps: Question - ): RelatedQuestions => { + ): RelatedQuestion[] => { const {relatedQuestions} = questionProps const onSiteQuestions = onSiteQuestionsRef.current diff --git a/app/hooks/useToC.tsx b/app/hooks/useToC.tsx index cc5730bb..9e28c014 100644 --- a/app/hooks/useToC.tsx +++ b/app/hooks/useToC.tsx @@ -5,6 +5,7 @@ const identity = (i: any) => i const useToC = () => { const {items: toc} = useCachedToC() + console.log(toc) const checkPath = (pageid: string) => (item: TOCItem) => { if (item.pageid === pageid) return [pageid] @@ -21,10 +22,32 @@ const useToC = () => { return toc.map(checkPath(pageid)).filter(identity)[0] } + const getNext = (pageid: string): TOCItem | undefined => { + type NextItem = { + current?: string + next?: TOCItem + } + const findNext = (prev: string | undefined, item: TOCItem): NextItem => { + if (pageid === prev) return {current: prev, next: item} + + let previous: string | undefined = item.pageid + for (const child of item.children || []) { + const {next, current} = findNext(previous, child) + if (next) return {next, current} + previous = current + } + return {current: previous} + } + + const all = {pageid: '', children: toc || []} as TOCItem + return toc && findNext('', all).next + } + return { toc: toc || [], findSection, getPath, + getNext, } } diff --git a/app/routes/questions.add.tsx b/app/routes/questions.add.tsx index b8b2d6f8..4dfa6748 100644 --- a/app/routes/questions.add.tsx +++ b/app/routes/questions.add.tsx @@ -2,9 +2,9 @@ import {useState, useEffect} from 'react' import type {ActionFunctionArgs} from '@remix-run/cloudflare' import {Form} from '@remix-run/react' import {redirect} from '@remix-run/cloudflare' -import {addQuestion, loadAllQuestions, fetchJsonList, RelatedQuestions} from '~/server-utils/stampy' +import {addQuestion, loadAllQuestions, fetchJsonList, RelatedQuestion} from '~/server-utils/stampy' -const getRelated = async (question: string): Promise => { +const getRelated = async (question: string): Promise => { const url = `${NLP_SEARCH_ENDPOINT}/api/search?query=${question}?status=all` try { return await fetchJsonList(url) @@ -41,7 +41,7 @@ export const action = async ({request}: ActionFunctionArgs) => { } else { relatedQuestions = formData .getAll('relatedQuestion') - .map((question) => ({title: question})) as RelatedQuestions + .map((question) => ({title: question})) as RelatedQuestion[] } // Make sure the question is formatted as a question diff --git a/app/routes/tags.single.$tag.tsx b/app/routes/tags.single.$tag.tsx index 0193167a..9d88e299 100644 --- a/app/routes/tags.single.$tag.tsx +++ b/app/routes/tags.single.$tag.tsx @@ -1,7 +1,7 @@ import {useState, useEffect, ReactNode} from 'react' import {LoaderFunction} from '@remix-run/cloudflare' import {reloadInBackgroundIfNeeded} from '~/server-utils/kv-cache' -import {Tag as TagType, QuestionState, RelatedQuestions, loadTag} from '~/server-utils/stampy' +import {Tag as TagType, QuestionState, RelatedQuestion, loadTag} from '~/server-utils/stampy' import Dialog from '~/components/dialog' type Props = { @@ -47,7 +47,7 @@ export function Tag({ showCount, }: { name: string - questions?: RelatedQuestions + questions?: RelatedQuestion[] showCount?: boolean }) { const [questions, setQuestions] = useState(tqs) diff --git a/app/server-utils/stampy.ts b/app/server-utils/stampy.ts index dad1eeb1..ab7c1c5c 100644 --- a/app/server-utils/stampy.ts +++ b/app/server-utils/stampy.ts @@ -26,7 +26,7 @@ export enum QuestionState { COLLAPSED = '-', RELATED = 'r', } -export type RelatedQuestions = {title: string; pageid: string}[] +export type RelatedQuestion = {title: string; pageid: string} export enum QuestionStatus { WITHDRAWN = 'Withdrawn', SKETCH = 'Bulletpoint sketch', @@ -59,7 +59,7 @@ export type Tag = { name: string url: string internal: boolean - questions: RelatedQuestions + questions: RelatedQuestion[] mainQuestion: string | null } export type Question = { @@ -67,7 +67,7 @@ export type Question = { pageid: string text: string | null answerEditLink: string | null - relatedQuestions: RelatedQuestions + relatedQuestions: RelatedQuestion[] questionState?: QuestionState tags: string[] banners: Banner[] @@ -82,7 +82,7 @@ export type Question = { export type PageId = Question['pageid'] export type NewQuestion = { title: string - relatedQuestions: RelatedQuestions + relatedQuestions: RelatedQuestion[] source?: string } type Entity = { @@ -434,7 +434,7 @@ export const insertRows = async (table: string, rows: NewQuestion[]) => { return await sendToCoda(url, payload, 'POST', `${CODA_INCOMING_TOKEN}`) } -export const addQuestion = async (title: string, relatedQuestions: RelatedQuestions) => { +export const addQuestion = async (title: string, relatedQuestions: RelatedQuestion[]) => { return await insertRows(INCOMING_QUESTIONS_TABLE, [{title, relatedQuestions}]) } diff --git a/stories/ArticlesKeepGoing.stories.tsx b/stories/ArticlesKeepGoing.stories.tsx index 2e35a093..29f4ab6d 100644 --- a/stories/ArticlesKeepGoing.stories.tsx +++ b/stories/ArticlesKeepGoing.stories.tsx @@ -1,32 +1,110 @@ import type {Meta, StoryObj} from '@storybook/react' -import {ArticleKeepGoing} from '../app/components/ArticleKeepGoing' -import SvgArrowRight from '../app/components/icons-generated/ArrowRight' -import {ArticlesNav} from '~/components/ArticlesNav/Menu' +import KeepGoing from '../app/components/Article/KeepGoing' +import {CachedObjectsContext} from '../app/hooks/useCachedObjects' +import type {TOCItem} from '../app/routes/questions.toc' +import type {Question} from '../app/server-utils/stampy' + +const toc = { + title: 'New to AI safety? Start here.', + pageid: '9OGZ', + hasText: true, + category: 'Your momma', + children: [ + { + title: 'What would an AGI be able to do?', + pageid: 'NH51', + hasText: false, + }, + { + title: 'Types of AI', + pageid: 'NH50', + hasText: false, + children: [ + { + title: 'What are the differences between AGI, transformative AI, and superintelligence?', + pageid: '5864', + hasText: true, + }, + { + title: 'What is intelligence?', + pageid: '6315', + hasText: true, + }, + ], + }, + { + title: 'Introduction to ML', + pageid: 'NH50', + hasText: false, + children: [ + { + title: 'What are large language models?', + pageid: '8161', + hasText: true, + }, + { + title: 'What is compute?', + pageid: '9358', + hasText: true, + }, + ], + }, + ], +} as any as TOCItem + +const withMockedToC = (StoryFn: any) => { + return ( + + + + ) +} const meta = { - title: 'Components/ArticleKeepGoing', - component: ArticleKeepGoing, + title: 'Components/Article/KeepGoing', + component: KeepGoing, tags: ['autodocs'], -} satisfies Meta + decorators: [withMockedToC], +} satisfies Meta export default meta -type Story = StoryObj +type Story = StoryObj -export const Primary: Story = { +export const Default: Story = { args: { - category: 'AI alignment', - articles: [ - {title: 'What is AI alignment', pageid: '1231', hasIcon: true}, - {title: 'What is this', pageid: '1232', hasIcon: true}, - {title: 'What is AI safety', pageid: '1233', hasIcon: true}, - {title: 'What is that', pageid: '1234', hasIcon: true}, - {title: 'What is the the orthogonality thesis', pageid: '1235', hasIcon: true}, - {title: 'What is something else', pageid: '1236', hasIcon: true}, + pageid: 'NH50', + relatedQuestions: [ + {pageid: '1412', title: 'something or other'}, + {pageid: '1234', title: 'Another related question'}, + {pageid: '1235', title: 'How about this one?'}, + {pageid: '1236', title: 'What time is it?'}, ], - next: { - title: - 'Are there any AI alignment projects which governments could usefully put a very large amount of resources into?', - pageid: '1235', - icon: , - }, - }, + } as any as Question, +} + +export const NoMore: Story = { + args: { + pageid: '123', + relatedQuestions: [], + } as any as Question, +} + +export const OnlyRelated: Story = { + args: { + pageid: '123', + relatedQuestions: [ + {pageid: '1412', title: 'something or other'}, + {pageid: '1234', title: 'Another related question'}, + {pageid: '1235', title: 'How about this one?'}, + {pageid: '1236', title: 'What time is it?'}, + ], + } as any as Question, +} + +export const OnlyNext: Story = { + args: { + pageid: 'NH50', + relatedQuestions: [], + } as any as Question, }