diff --git a/app/components/Article/KeepGoing/index.tsx b/app/components/Article/KeepGoing/index.tsx index 96c9cb52..9467c9a5 100644 --- a/app/components/Article/KeepGoing/index.tsx +++ b/app/components/Article/KeepGoing/index.tsx @@ -4,6 +4,7 @@ import {ArrowRight} from '~/components/icons-generated' import useToC from '~/hooks/useToC' import type {TOCItem} from '~/routes/questions.toc' import type {Question, RelatedQuestion} from '~/server-utils/stampy' +import {questionUrl} from '~/routesMapper' import styles from './keepGoing.module.css' const nonContinueSections = ['8TJV'] @@ -18,12 +19,12 @@ const NextArticle = ({section, next, first}: NextArticleProps) => <>

Keep going! 👉

- {first ? 'Start' : 'Continue'} with the {first ? 'first' : 'next'} article in{' '} - {section?.category} + {first ? 'Start' : 'Continue'} with the {first ? 'first' : 'next'} article in " + {section?.title}"
{next.title}
- @@ -38,11 +39,11 @@ export const KeepGoing = ({pageid, relatedQuestions}: Question) => { const hasRelated = relatedQuestions && relatedQuestions.length > 0 const skipNext = nonContinueSections.includes(section?.pageid || '') - const formatRelated = (related: RelatedQuestion) => { + const formatRelated = (hasIcon: boolean) => (related: RelatedQuestion) => { const relatedSection = findSection(related.pageid) const subtitle = relatedSection && relatedSection.pageid !== section?.pageid ? relatedSection.title : undefined - return {...related, subtitle, hasIcon: true} + return {...related, subtitle, hasIcon} } return ( @@ -56,12 +57,15 @@ export const KeepGoing = ({pageid, relatedQuestions}: Question) => { )} {hasRelated && !skipNext && (
- +
)} {skipNext && (
- +
)}
diff --git a/app/components/Article/index.tsx b/app/components/Article/index.tsx index ae062a92..b441447e 100644 --- a/app/components/Article/index.tsx +++ b/app/components/Article/index.tsx @@ -6,6 +6,7 @@ import EditIcon from '~/components/icons-generated/Pencil' import Button, {CompositeButton} from '~/components/Button' import {Action, ActionType} from '~/routes/questions.actions' import type {Glossary, Question} from '~/server-utils/stampy' +import {tagUrl} from '~/routesMapper' import Contents from './Contents' import './article.css' @@ -86,7 +87,7 @@ const ArticleMeta = ({question, className}: {question: Question; className?: str const Tags = ({tags}: Question) => (
{tags?.map((tag) => ( - + {tag} ))} diff --git a/app/components/ArticlesDropdown/index.tsx b/app/components/ArticlesDropdown/index.tsx index ebed05d6..2e1efefd 100644 --- a/app/components/ArticlesDropdown/index.tsx +++ b/app/components/ArticlesDropdown/index.tsx @@ -2,7 +2,8 @@ import {useState, useEffect} from 'react' import {Link as LinkElem} from '@remix-run/react' import type {Tag} from '~/server-utils/stampy' import {TOCItem, Category, ADVANCED, INTRODUCTORY} from '~/routes/questions.toc' -import {buildTagUrl, sortFuncs} from '~/routes/tags.$' +import {sortFuncs} from '~/routes/tags.$' +import {questionUrl, tagsUrl, tagUrl} from '~/routesMapper' import Button from '~/components/Button' import './dropdown.css' @@ -41,8 +42,8 @@ export const ArticlesDropdown = ({toc, categories}: ArticlesDropdownProps) => {
{category}
{toc .filter((item) => item.category === category) - .map(({pageid, title}: TOCItem) => ( - + .map((item: TOCItem) => ( + ))}
) @@ -65,12 +66,12 @@ export const ArticlesDropdown = ({toc, categories}: ArticlesDropdownProps) => { ))} - diff --git a/app/components/ArticlesNav/Menu.tsx b/app/components/ArticlesNav/Menu.tsx index 868c3a0f..ed793984 100644 --- a/app/components/ArticlesNav/Menu.tsx +++ b/app/components/ArticlesNav/Menu.tsx @@ -1,4 +1,5 @@ import {Link} from '@remix-run/react' +import {questionUrl} from '~/routesMapper' import type {TOCItem} from '~/routes/questions.toc' import './menu.css' @@ -19,7 +20,7 @@ const Title = ({article, path, current}: Article) => { const selectedClass = article?.pageid === current ? ' selected' : '' if (article.pageid === (path && path[0])) { return ( - +
{article?.title}
@@ -28,7 +29,7 @@ const Title = ({article, path, current}: Article) => { } return ( - {!article.hasText ? article.title : {article.title}} + {!article.hasText ? article.title : {article.title}} ) diff --git a/app/components/CategoriesNav/Menu.tsx b/app/components/CategoriesNav/Menu.tsx index f068c186..2839a0fb 100644 --- a/app/components/CategoriesNav/Menu.tsx +++ b/app/components/CategoriesNav/Menu.tsx @@ -2,6 +2,7 @@ import {useState} from 'react' import {Link} from '@remix-run/react' import {SearchInput} from '../SearchInput/Input' import {Tag as TagType} from '~/server-utils/stampy' +import {tagUrl} from '~/routesMapper' import styles from './menu.module.css' interface CategoriesNavProps { @@ -28,7 +29,7 @@ export const CategoriesNav = ({categories, activeCategoryId}: CategoriesNavProps .map(({tagId, name, questions}) => ( ( } - action="/9OGZ" + action={questionUrl({pageid: '9OGZ'})} actionTitle={ <> Start here @@ -65,8 +66,13 @@ export const ContentBoxMain = () => ( ) export const ContentBoxSecond = () => { + const article = {pageid: '9TDI', title: 'Not convinced? Explore the arguments.'} return ( - + { } export const ContentBoxThird = () => { + const article = {pageid: '8TJV', title: 'Get involved with AI safety'} return ( diff --git a/app/components/Error/error.module.css b/app/components/Error/error.module.css index c818fc4d..bab111ad 100644 --- a/app/components/Error/error.module.css +++ b/app/components/Error/error.module.css @@ -1,3 +1,4 @@ .errorContainer { margin: var(--spacing-24) auto; + height: 100%; } diff --git a/app/components/Error/index.tsx b/app/components/Error/index.tsx index 27bcc63e..237cce6e 100644 --- a/app/components/Error/index.tsx +++ b/app/components/Error/index.tsx @@ -1,7 +1,13 @@ import styles from './error.module.css' const errors = { - 404: 'Sorry, this page was not found. Please go to the Discord server and report where you found this link.', + 404: ( + <> + Sorry, this page was not found. Please go to the{' '} + Discord server and report where you found + this link. + + ), 500: 'Sorry, something bad happened. Please retry', emptyArticle: 'Sorry, it looks like this article could not be fetched', } @@ -13,7 +19,7 @@ type ErrorType = { const Error = ({error}: {error?: ErrorType}) => { return ( -
+

{error?.statusText}

{error?.status &&
{errors[error.status as keyof typeof errors] || ''}
}
diff --git a/app/components/Grid/index.tsx b/app/components/Grid/index.tsx index 9d73a6f7..1170fda0 100644 --- a/app/components/Grid/index.tsx +++ b/app/components/Grid/index.tsx @@ -1,8 +1,9 @@ import type {TOCItem} from '~/routes/questions.toc' +import {questionUrl} from '~/routesMapper' import './grid.css' export const GridBox = ({title, subtitle, icon, pageid}: TOCItem) => ( - + {icon && {title}}
{title}
{subtitle}
diff --git a/app/components/Table/index.tsx b/app/components/Table/index.tsx index 1e521add..33497be0 100644 --- a/app/components/Table/index.tsx +++ b/app/components/Table/index.tsx @@ -1,5 +1,6 @@ import {Link} from '@remix-run/react' import {ArrowUpRight} from '~/components/icons-generated' +import {questionUrl} from '~/routesMapper' import styles from './listTable.module.css' export type ListItem = { @@ -13,18 +14,19 @@ export type ListTableProps = { * Browse by category */ elements: ListItem[] + sameTab?: boolean className?: string } -export const ListTable = ({elements, className}: ListTableProps) => ( +export const ListTable = ({elements, sameTab, className}: ListTableProps) => (
{elements.map(({pageid, title, subtitle, hasIcon}, i) => (
{title}
diff --git a/app/components/Widget/Stampy.tsx b/app/components/Widget/Stampy.tsx index e796b57b..cddec732 100644 --- a/app/components/Widget/Stampy.tsx +++ b/app/components/Widget/Stampy.tsx @@ -1,47 +1,52 @@ +import {useState} from 'react' +import {Link} from '@remix-run/react' import StampyIcon from '~/components/icons-generated/Stampy' import SendIcon from '~/components/icons-generated/PlaneSend' +import Button from '~/components/Button' import './stampy.css' export const WidgetStampy = () => { + const [question, setQuestion] = useState('') + const questions = [ + 'Why couldn’t we just turn the AI off?', + 'How would the AI even get out in the world?', + 'Do people seriously worry about existential risk from AI?', + ] return ( -
-
-

Questions?

-

Ask Stampy any question about AI Safety

+
+
+

Questions?

+

Ask Stampy any question about AI Safety

-
-
- -
-
Try asking me...
- {/**/} -
-
Why couldn’t we just turn the AI off?
+
+ +
+
Try asking me...
+ {questions.map((question, i) => ( +
+
-
-
How would the AI even get out in the world?
-
-
-
- Do people seriously worry about existential risk from AI? -
-
-
+ ))}
-
-
- -
-
+
+ setQuestion(e.target.value)} + /> + -
+
) diff --git a/app/components/Widget/stampy.css b/app/components/Widget/stampy.css index 72a7b983..fea5eb04 100644 --- a/app/components/Widget/stampy.css +++ b/app/components/Widget/stampy.css @@ -1,160 +1,25 @@ -@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap'); - -.widget-title { - margin-block-start: 0; - margin-block-end: 0px; -} -.widget-subtitle { - margin: 0; - color: var(--text-black); -} -.widget-header { - /*position: absolute;*/ - top: 0px; - left: 0px; - font-size: 38px; - letter-spacing: -0.01em; - line-height: 130%; - font-weight: 600; - display: inline-block; - max-width: 562px; - margin-block-end: 38px; -} -.widget-input { - /*position: absolute;*/ - height: 100%; - width: 100%; - top: 0%; - right: 0%; - bottom: 0%; - left: 0%; - border-radius: 6px; - background-color: #fff; - box-shadow: 0px 16px 40px rgba(175, 183, 194, 0.2); - border: 1px solid var(--colors-cool-grey-200); - box-sizing: border-box; - letter-spacing: -0.01em; - text-align: left; - color: var(--colors-cool-grey-600); - padding-left: 16px; - padding-right: 66px; -} -.input-label { - position: relative; - letter-spacing: -0.01em; +.chat { + margin-left: 6vw; } -.input-horizontal { - position: absolute; - top: calc(50% - 15px); - left: 16px; + +.sample-messages-container { display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; -} -.widget-textbox { - /*position: absolute;*/ - top: calc(50% - 32px); - left: calc(50% - 435px); - width: 870px; - height: 64px; -} -.send-button { - /*position: absolute;*/ - top: calc(50% - 24px); - left: 0px; - border-radius: 6px; - background-color: var(--colors-teal-500); - box-shadow: 0px 16px 40px rgba(32, 44, 89, 0.05); - width: 48px; - height: 48px; -} -.send-button-icon { - position: sticky; - top: 25%; - right: 25%; - bottom: 25%; - left: 25%; - max-width: 100%; - overflow: hidden; - max-height: 100%; + align-items: flex-end; + gap: var(--spacing-16); } -.send-button-group { - position: relative; - width: 48px; - height: 48px; - z-index: 2; - left: -53px; + +.sample-messages { + width: 100%; + padding: var(--spacing-32); + background: var(--colors-cool-grey-200); } -.widget-ask { - /*position: absolute;*/ - top: calc(50% + 233px); - left: calc(50% - 435px); - width: 870px; - height: 64px; - color: var(--colors-cool-grey-600); + +.widget-ask input { display: inline-flex; - align-items: center; -} -.rectangleIcon { - position: absolute; - top: 0px; - left: 0px; - width: 814px; - height: 319px; -} -.widget-start-conversation { - /*position: absolute;*/ - top: 87px; - left: 32px; - border-radius: 6px; - background-color: #fff; - box-shadow: 0px 16px 40px rgba(32, 44, 89, 0.05); - border: 1px solid var(--colors-cool-grey-200); - box-sizing: border-box; - height: 56px; - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; + height: var(--spacing-56, 56px); padding: 7px 21px; - margin: 16px; - width: fit-content; -} -.widget-conversation-start-header { - /*position: absolute;*/ - top: 32px; - left: 32px; - letter-spacing: -0.01em; - color: var(--text-black); - margin: 16px; -} -.widget-chat-group { - /*position: absolute;*/ - top: 187px; - left: 56px; - width: 814px; - height: 319px; -} -.stampyIcon { - width: 40px; - height: 34.56px; - overflow: hidden; - margin-bottom: 16px; } -.widget-group { - width: 100%; - position: relative; - height: 594px; - text-align: left; - color: var(--colors-teal-500); -} -.chat-message { - display: flex; - align-items: flex-end; -} -.chat-incoming-message { - background: #f9fafc; - width: 100%; - border-radius: 6px; +.widget-ask svg { + position: absolute; + transform: translate(-52px, 4px); } diff --git a/app/components/errorHandling.tsx b/app/components/errorHandling.tsx index 9c1cb5c5..e69de29b 100644 --- a/app/components/errorHandling.tsx +++ b/app/components/errorHandling.tsx @@ -1,25 +0,0 @@ -import {Component, ErrorInfo} from 'react' - -type Props = {title: string; children: JSX.Element[]} -type State = {hasError: boolean} -export default class ErrorBoundary extends Component { - constructor(props: Props) { - super(props) - this.state = {hasError: false} - } - - static getDerivedStateFromError() { - return {hasError: true} - } - - componentDidCatch(error: Error, errorInfo: ErrorInfo) { - console.log(error, errorInfo) - } - - render() { - if (this.state.hasError) { - return

Error loading "{this.props.title}"

- } - return this.props.children - } -} diff --git a/app/components/search.tsx b/app/components/search.tsx index a3002e81..27978ae0 100644 --- a/app/components/search.tsx +++ b/app/components/search.tsx @@ -3,8 +3,9 @@ import debounce from 'lodash/debounce' import {useSearch} from '~/hooks/useSearch' import {Question} from '~/server-utils/stampy' import {SearchInput} from './SearchInput/Input' -import {fetchAllQuestionsOnSite} from '~/routes/questions.allQuestionsOnSite' import {SearchResults} from './SearchResults/Dropdown' +import {fetchAllQuestionsOnSite} from '~/routes/questions.allQuestionsOnSite' +import {questionUrl} from '~/routesMapper' type Props = { queryFromUrl?: string @@ -82,7 +83,7 @@ export default function Search({queryFromUrl, limitFromUrl, removeQueryFromUrl}: ({ title: r.title, - url: `/${r.pageid}`, + url: questionUrl(r), description: '', // TODO: fetch descriptions 🤔 }))} /> diff --git a/app/hooks/useToC.tsx b/app/hooks/useToC.tsx index 306b30ea..63bae65c 100644 --- a/app/hooks/useToC.tsx +++ b/app/hooks/useToC.tsx @@ -47,7 +47,7 @@ const useToC = () => { return {current: previous} } - return toc && findNext('', all).next + return toc?.map((section) => findNext('', section).next).filter(identity)[0] } return { diff --git a/app/newRoot.css b/app/newRoot.css index 5b8a5f4c..fed4531d 100644 --- a/app/newRoot.css +++ b/app/newRoot.css @@ -259,6 +259,9 @@ h2 { width: 66.88vw; } +.full-width { + width: 100%; +} /* other tags */ a { @@ -311,6 +314,11 @@ ul { } /* all other classes */ +.rounded { + border-radius: 6px; + box-shadow: 0px 16px 40px rgba(175, 183, 194, 0.2); + box-sizing: border-box; +} .bordered { border-radius: 6px; diff --git a/app/root.tsx b/app/root.tsx index cbbaef4b..36655a91 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -183,6 +183,7 @@ export function ErrorBoundary() { + ) } diff --git a/app/routes/redirects.$redirects.tsx b/app/routes/$redirects.tsx similarity index 100% rename from app/routes/redirects.$redirects.tsx rename to app/routes/$redirects.tsx diff --git a/app/routes/[sitemap.xml].tsx b/app/routes/[sitemap.xml].tsx index d715644c..ef04254b 100644 --- a/app/routes/[sitemap.xml].tsx +++ b/app/routes/[sitemap.xml].tsx @@ -1,12 +1,13 @@ import {LoaderFunctionArgs} from '@remix-run/cloudflare' -import {loadAllQuestions, QuestionStatus, Question, QuestionState} from '~/server-utils/stampy' +import {loadAllQuestions, QuestionStatus, Question} from '~/server-utils/stampy' +import {questionUrl} from '~/routesMapper' export const loader = async ({request}: LoaderFunctionArgs) => { const origin = new URL(request.url).origin - const formatQuestion = ({pageid, updatedAt}: Question) => ` + const formatQuestion = (question: Question) => ` - ${origin}/?state=${pageid}${QuestionState.OPEN} - ${updatedAt} + ${origin}${questionUrl(question)} + ${question.updatedAt} 1.0 ` diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 28d73e1a..7d313b04 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -7,6 +7,7 @@ import useToC from '~/hooks/useToC' import Grid from '~/components/Grid' import Page from '~/components/Page' import {getStateEntries} from '~/hooks/stateModifiers' +import {questionUrl} from '~/routesMapper' export const loader = async ({request}: Parameters[0]) => { const url = new URL(request.url) @@ -17,7 +18,7 @@ export const loader = async ({request}: Parameters[0]) => { )[0]?.[0] if (firstOpenId) { url.searchParams.delete('state') - url.pathname = `/${firstOpenId}` + url.pathname = questionUrl({pageid: firstOpenId}) throw redirect(url.toString()) } } @@ -40,6 +41,7 @@ export default function App() { +
diff --git a/app/routes/$questionId.tsx b/app/routes/questions.$questionId.$.tsx similarity index 56% rename from app/routes/$questionId.tsx rename to app/routes/questions.$questionId.$.tsx index 87e70c85..bfb400d6 100644 --- a/app/routes/$questionId.tsx +++ b/app/routes/questions.$questionId.$.tsx @@ -1,15 +1,56 @@ import {Await, useLoaderData, useParams} from '@remix-run/react' +import {defer, type LoaderFunctionArgs} from '@remix-run/cloudflare' import {Suspense, useEffect, useState} from 'react' import Page from '~/components/Page' -import {loader} from '~/routes/questions.$questionId' -import {ArticlesNav} from '~/components/ArticlesNav/Menu' import Article from '~/components/Article' import Error from '~/components/Error' +import {ArticlesNav} from '~/components/ArticlesNav/Menu' import {fetchGlossary} from '~/routes/questions.glossary' +import {loadQuestionDetail, loadTags} from '~/server-utils/stampy' import useToC from '~/hooks/useToC' import type {Question, Glossary} from '~/server-utils/stampy' +import {reloadInBackgroundIfNeeded} from '~/server-utils/kv-cache' + +export const LINK_WITHOUT_DETAILS_CLS = 'link-without-details' + +const raise500 = (error: Error) => new Response(error.toString(), {status: 500}) + +export const loader = async ({request, params}: LoaderFunctionArgs) => { + const {questionId} = params + if (!questionId) { + throw new Response('Missing question title', {status: 400}) + } -export {loader} + try { + const dataPromise = loadQuestionDetail(request, questionId) + .then(({data}) => data) + .catch(raise500) + const tagsPromise = loadTags(request) + .then(({data}) => data) + .catch(raise500) + return defer({data: dataPromise, tags: tagsPromise}) + } catch (error: unknown) { + 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}) + } +} + +export function fetchQuestion(pageid: string) { + const url = `/questions/${encodeURIComponent(pageid)}` + return fetch(url) + .then(async (response) => { + const json: Awaited> = await response.json() + if ('error' in json) console.error(json.error) + const {data, timestamp} = json + + reloadInBackgroundIfNeeded(url, timestamp) + + return data + }) + .catch((e) => { + throw raise500(e) + }) +} const dummyQuestion = (title: string | undefined) => ({ diff --git a/app/routes/questions.$questionId.tsx b/app/routes/questions.old.$questionId.tsx similarity index 100% rename from app/routes/questions.$questionId.tsx rename to app/routes/questions.old.$questionId.tsx diff --git a/app/routes/tags.$.tsx b/app/routes/tags.$.tsx index 619518ba..10596438 100644 --- a/app/routes/tags.$.tsx +++ b/app/routes/tags.$.tsx @@ -8,8 +8,6 @@ import type {Tag as TagType} from '~/server-utils/stampy' export {loader} -export const buildTagUrl = ({tagId, name}: TagType) => `/tags/${tagId}/${name}` - export const sortFuncs = { alphabetically: (a: TagType, b: TagType) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()), diff --git a/app/routes/tags.all.tsx b/app/routes/tags.all.tsx index 59c65599..99301768 100644 --- a/app/routes/tags.all.tsx +++ b/app/routes/tags.all.tsx @@ -1,5 +1,6 @@ import {LoaderFunction} from '@remix-run/cloudflare' import {loadTags} from '~/server-utils/stampy' +import {allTagsUrl} from '~/routesMapper' import {reloadInBackgroundIfNeeded} from '~/server-utils/kv-cache' export const loader = async ({request, params}: Parameters[0]) => { @@ -21,7 +22,7 @@ export const loader = async ({request, params}: Parameters[0]) = } export const fetchTags = () => { - const url = `/tags/all` + const url = allTagsUrl() return fetch(url).then(async (response) => { const json: Awaited> = await response.json() if ('error' in json) console.error(json.error) diff --git a/app/routesMapper.ts b/app/routesMapper.ts new file mode 100644 index 00000000..5f65edea --- /dev/null +++ b/app/routesMapper.ts @@ -0,0 +1,7 @@ +export const questionUrl = ({pageid, title}: {pageid: string; title?: string}) => + `/questions/${pageid}/${title || ''}` + +export const tagUrl = ({tagId, name}: {tagId?: number | string; name: string}) => + tagId ? `/tags/${tagId}/${name}` : `/tags/${name}` +export const tagsUrl = () => `/tags/` +export const allTagsUrl = () => `/tags/all` diff --git a/remix.config.js b/remix.config.js index 5d53de06..c6d3fca3 100644 --- a/remix.config.js +++ b/remix.config.js @@ -1,5 +1,12 @@ +const routes = (defineRoutes) => + defineRoutes((route) => { + console.log(route) + route('/*', 'routes/$redirects.tsx') + }) + /** @type {import('@remix-run/dev').AppConfig} */ module.exports = { + routes, ignoredRouteFiles: ['**/.*'], serverConditions: ['workerd', 'worker', 'browser'], serverMainFields: ['browser', 'module', 'main'],