Skip to content

Commit

Permalink
add embed parameter for inclusion on other websites #307
Browse files Browse the repository at this point in the history
* strip header, footer, initial questions, and discord
* allow override of search placeholder
* fix "None of these" button when inside iframe
  • Loading branch information
Aprillion committed Sep 2, 2023
1 parent e424db0 commit 064d6ee
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 46 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ Stampy UI is an interface for [aisafety.info](https://aisafety.info), a question

Contributions are welcome, the code is released under the MIT License. If you'd like to join the [dev team](https://coda.io/d/AI-Safety-Info_dfau7sl2hmG/Dev-team_sulmV#_luYjG), drop by [our Discord](https://discord.com/invite/7wjJbFJnSN) and post in #stampy-dev!

## Supported URL parameters

- state - controls which questions are displayed as collapsed / open / related, e.g. `aisafety.info/?state=6568_897Ir6220r`
- embed - show site without header/footer for embedding on other sites, e.g. [embed-example.html](/public/embed-example.html)
- placeholder (string) - override `<input placeholder=...>` of the search box
- theme (light|dark) - override CSS theme (if not provided, the embedded site will use `preferred-color-scheme` system setting)
- showInitial - also show initial questions, not just search bar
- more (disabled|infini|button|buttonInfini) - debug versions of load more / infinite scroll, e.g. `aisafety.info/?more=infini`

## Stampy UI Development Setup

1. Requirements
Expand Down Expand Up @@ -88,6 +97,8 @@ Production domains are deployed via GitHub Actions.

## Add a new domain

If the same CF worker should be reachable from another domain:

- log in to [Cloudflare Dashboard](https://dash.cloudflare.com/) owned by @plexish
- use `Add a site` button on homepage, choose the Free plan
- in the DNS section for this site > `Add record` for 2 new CNAME records:
Expand Down
9 changes: 8 additions & 1 deletion app/components/layouts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import logoFunSvg from '../assets/stampy-logo.svg'
import logoMinSvg from '../assets/stampy-logo-min.svg'
import {Share, Users, Code, Tag} from './icons-generated'
import CopyLink from './copyLink'
import type {Context} from '~/root'

const year = new Date().getFullYear()

export const Header = ({reset = () => null}: {reset?: (e: MouseEvent) => void}) => {
const minLogo = useOutletContext<boolean>()
const {minLogo, embed} = useOutletContext<Context>()

if (embed) return null

return (
<header className={minLogo ? 'min-logo' : 'fun-logo'}>
Expand Down Expand Up @@ -55,6 +58,10 @@ export const Header = ({reset = () => null}: {reset?: (e: MouseEvent) => void})
}

export const Footer = () => {
const {embed} = useOutletContext<Context>()

if (embed) return null

return (
<footer>
<a href="https://coda.io/d/AI-Safety-Info-Dashboard_dfau7sl2hmG/Copyright_su79L#_luPMa">
Expand Down
6 changes: 5 additions & 1 deletion app/components/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {MagnifyingGlass, Edit} from '~/components/icons-generated'
import {useSearch, Question as QuestionType, SearchResult} from '~/hooks/search'
import AutoHeight from 'react-auto-height'
import Dialog from '~/components/dialog'
import {useSearchParams} from '@remix-run/react'

type Props = {
onSiteAnswersRef: MutableRefObject<QuestionType[]>
Expand All @@ -20,6 +21,9 @@ export default function Search({onSiteAnswersRef, openQuestionTitles, onSelect}:
const [showMore, setShowMore] = useState(false)
const searchInputRef = useRef('')

const [urlSearchParams] = useSearchParams()
const placeholder = urlSearchParams.get('placeholder') ?? 'Search for more questions here...'

const {search, arePendingSearches, results} = useSearch(onSiteAnswersRef)

const searchFn = (rawValue: string) => {
Expand Down Expand Up @@ -61,7 +65,7 @@ export default function Search({onSiteAnswersRef, openQuestionTitles, onSelect}:
<input
type="search"
name="searchbar"
placeholder="Search for more questions here..."
placeholder={placeholder}
autoComplete="off"
onChange={(e) => handleChange(e.currentTarget.value)}
onKeyDown={(e) => e.key === 'Enter' && searchFn(e.currentTarget.value)}
Expand Down
2 changes: 1 addition & 1 deletion app/hooks/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const isSupported = (theme: string | null | undefined): theme is string | undefi
theme !== null && supported.has(theme)

export const useTheme = () => {
const [searchParams] = useSearchParams() // url parameter for iframe usage: ...?embedded&theme=dark
const [searchParams] = useSearchParams() // url parameter for iframe usage: ...?embed&theme=dark
const [savedTheme, setSavedTheme] = useState(() => {
const fromParams = searchParams.get(THEME)
if (isSupported(fromParams)) return fromParams
Expand Down
41 changes: 22 additions & 19 deletions app/root.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
box-sizing: border-box;
}

:root, :root.light {
:root,
:root.light {
/* light theme: */
--bgColorPage: #eee;
--bgColorQuestionTitle: #bdf;
Expand Down Expand Up @@ -104,7 +105,9 @@ a:hover,
}

/* reset default styles if user prefers light/dark mode, but chooses dark/light mode in the toggle */
input, textarea, select {
input,
textarea,
select {
background: var(--bgColorInput);
color: var(--colorText);
border: 1px solid var(--borderColorButton);
Expand Down Expand Up @@ -820,23 +823,23 @@ a[target='_blank']:not(.icon-link):after {
@media (prefers-color-scheme: dark) {
:root {
/* duplicate to avoid flash of white background while JS loads, consider some CSS postprocessing solution if this needs to be maintainable */
--bgColorPage: #333;
--bgColorQuestionTitle: #30386e;
--bgColorQuestionAnswer: #171f29;
--bgColorCopied: #fed;
--colorText: #f7fff9;
--colorTitle: #f7fff9;
--colorTitleHighlight: #d86;
--colorQuestionTitle: #ccc;
--borderColor: #444;
--borderColorButton: #666;
--bgColorInput: #2b2b2b;
--bgColorButton: #222;
--bgColorHighlight: #354652;
--bgColorTableRows: #1d2436;
--colorLink: #89e3ff;
--colorLinkVisited: #79f;
--colorTooltip: #2e2e2e;
--bgColorPage: #333;
--bgColorQuestionTitle: #30386e;
--bgColorQuestionAnswer: #171f29;
--bgColorCopied: #fed;
--colorText: #f7fff9;
--colorTitle: #f7fff9;
--colorTitleHighlight: #d86;
--colorQuestionTitle: #ccc;
--borderColor: #444;
--borderColorButton: #666;
--bgColorInput: #2b2b2b;
--bgColorButton: #222;
--bgColorHighlight: #354652;
--bgColorTableRows: #1d2436;
--colorLink: #89e3ff;
--colorLinkVisited: #79f;
--colorTooltip: #2e2e2e;
}
}
:root.dark {
Expand Down
19 changes: 14 additions & 5 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ export const loader = async ({request}: Parameters<LoaderFunction>[0]) => {
const isFunLogoForcedOn = request.url.match(/funLogo/)
const minLogo = isDomainWithFunLogo ? !!isFunLogoForcedOff : !isFunLogoForcedOn

const embed = !!request.url.match(/embed/)

const question = await fetchQuestion(request).catch((e) => {
console.error('\n\nUnexpected error in loader\n', e)
return null
Expand All @@ -86,6 +88,7 @@ export const loader = async ({request}: Parameters<LoaderFunction>[0]) => {
question,
url: request.url,
minLogo,
embed,
}
}

Expand All @@ -94,8 +97,10 @@ function Head({minLogo}: {minLogo?: boolean}) {
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
{/* https://github.com/darkreader/darkreader/issues/1285#issuecomment-761893024 */}
<meta name="color-scheme" content="light dark" />
{/* don't use color-scheme because supporting transparent iframes https://fvsch.com/transparent-iframes
is more important than dark reader https://github.com/darkreader/darkreader/issues/1285#issuecomment-761893024
<meta name="color-scheme" content="light dark" />
*/}
<Meta />
<Links />
{minLogo ? (
Expand Down Expand Up @@ -124,15 +129,19 @@ export function ErrorBoundary({error}: {error: Error}) {
)
}

type Loader = Awaited<ReturnType<typeof loader>>
export type Context = Pick<Loader, 'minLogo' | 'embed'>

export default function App() {
const {minLogo} = useLoaderData<ReturnType<typeof loader>>()
const {minLogo, embed} = useLoaderData<Loader>()
const {savedTheme} = useTheme()
const context: Context = {minLogo, embed}

return (
<html lang="en" className={savedTheme}>
<html lang="en" className={`${embed ? 'embed' : ''} ${savedTheme ?? ''}`}>
<Head minLogo={minLogo} />
<body>
<Outlet context={minLogo} />
<Outlet context={context} />
{/* <ScrollRestoration /> wasn't doing anything useful */}
<Scripts />
<LiveReload />
Expand Down
33 changes: 22 additions & 11 deletions app/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,21 @@ import {Discord} from '~/components/icons-generated'
import InfiniteScroll from '~/components/infiniteScroll'
import ErrorBoundary from '~/components/errorHandling'
import {reloadInBackgroundIfNeeded} from '~/server-utils/kv-cache'
import type {Context} from '~/root'

const empty: Awaited<ReturnType<typeof loadInitialQuestions>> = {data: [], timestamp: ''}
export const loader = async ({request}: Parameters<LoaderFunction>[0]) => {
const embed = !!request.url.match(/embed/)
const showInitial = !!request.url.match(/showInitial/)
if (embed && !showInitial) return {initialQuestionsData: empty}

try {
await loadTags(request)
const initialQuestionsData = await loadInitialQuestions(request)
return {initialQuestionsData}
} catch (e) {
console.error(e)
return {initialQuestionsData: empty}
}
}

Expand Down Expand Up @@ -97,7 +104,7 @@ const Bottom = ({
}

export default function App() {
const minLogo = useOutletContext<boolean>()
const {minLogo, embed} = useOutletContext<Context>()
const {initialQuestionsData} = useLoaderData<ReturnType<typeof loader>>()
const {data: initialQuestions = [], timestamp} = initialQuestionsData ?? {}

Expand Down Expand Up @@ -209,16 +216,20 @@ export default function App() {
))}
</div>
</main>
<a id="discordChatBtn" href="https://discord.com/invite/Bt8PaRTDQC">
<Discord />
</a>

<Bottom
fetchMore={fetchMoreQuestions}
isSingleQuestion={
questions.filter((i) => i.questionState != QuestionState.RELATED).length == 1
}
/>
{!embed && (
<>
<a id="discordChatBtn" href="https://discord.com/invite/Bt8PaRTDQC">
<Discord />
</a>

<Bottom
fetchMore={fetchMoreQuestions}
isSingleQuestion={
questions.filter((i) => i.questionState != QuestionState.RELATED).length == 1
}
/>
</>
)}
</>
)
}
12 changes: 5 additions & 7 deletions app/routes/questions/add.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useState, useEffect} from 'react'
import type {ActionArgs} from '@remix-run/cloudflare'
import {Form, useSearchParams} from '@remix-run/react'
import {Form} from '@remix-run/react'
import {redirect} from '@remix-run/cloudflare'
import {addQuestion, loadAllQuestions, fetchJsonList, RelatedQuestions} from '~/server-utils/stampy'

Expand All @@ -17,7 +17,7 @@ export const action = async ({request}: ActionArgs) => {
const formData = await request.formData()
let title = formData.get('title') as string
const state = formData.get('stateString')
const redirectTo = '/' + (state ? '?state=' + state : '')
const redirectTo = '/' + state

// Make sure that the question was provided
if (!title) return redirect(redirectTo)
Expand Down Expand Up @@ -63,8 +63,6 @@ type Props = {

export const AddQuestion = ({title, relatedQuestions, immediately, ...props}: Props) => {
const url = '/questions/add'
const [remixSearchParams] = useSearchParams()
const [stateString] = useState(() => remixSearchParams.get('state') ?? '')
const [isSubmitted, setSubmitted] = useState(immediately)

const handleSubmit = async () => {
Expand All @@ -75,13 +73,13 @@ export const AddQuestion = ({title, relatedQuestions, immediately, ...props}: Pr
const addQuestion = async () => {
const body = new FormData()
body.append('title', title)
body.append('stateString', stateString)
body.append('stateString', location.search)
await fetch(url, {method: 'POST', body})
}
if (immediately) {
addQuestion()
}
}, [title, stateString, immediately])
}, [title, immediately])

if (isSubmitted) {
return (
Expand All @@ -106,7 +104,7 @@ export const AddQuestion = ({title, relatedQuestions, immediately, ...props}: Pr
{...props}
>
<input type="hidden" name="title" value={title} />
<input type="hidden" name="stateString" value={stateString} />
<input type="hidden" name="stateString" value={location.search} />
{relatedQuestions.map((title) => (
<input type="hidden" name="relatedQuestion" key={title} value={title} />
))}
Expand Down
3 changes: 2 additions & 1 deletion app/routes/random.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {loadAllQuestions, QuestionState, QuestionStatus} from '~/server-utils/st
import {Header, Footer} from '~/components/layouts'
import {Question} from '~/routes/questions/$question'
import useQuestionStateInUrl from '~/hooks/useQuestionStateInUrl'
import type {Context} from '~/root'

function randomItem(arr: any[]) {
return arr[Math.floor(Math.random() * arr.length)]
Expand All @@ -24,7 +25,7 @@ export const loader = async ({request}: Parameters<LoaderFunction>[0]) => {
}

export default function App() {
const minLogo = useOutletContext<boolean>()
const {minLogo} = useOutletContext<Context>()
const {initialQuestionsData} = useLoaderData<ReturnType<typeof loader>>()
const {data: initialQuestions = [], timestamp} = initialQuestionsData ?? {}

Expand Down
22 changes: 22 additions & 0 deletions public/embed-example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<div>Some content before embedding aisafety.info search...</div>

<iframe
src="/?embed&showInitial&placeholder=Search the AI Safety FAQ&theme=light"
width="100%"
height="330px"
frameborder="0"
id="ai-safety-search-iframe"
style="transition: height 0.3s; overflow: hidden;"
></iframe>

<div>Some content between...</div>

<script>
'use strict'
const iframe = document.getElementById('ai-safety-search-iframe')
const iframeWindow = iframe.contentWindow
iframeWindow.addEventListener('focus', () => {
iframe.style.height = '100vh'
})
</script>

0 comments on commit 064d6ee

Please sign in to comment.