diff --git a/apps/docs/components/Navigation/AlgoliaSearch.tsx b/apps/docs/components/Navigation/AlgoliaSearch.tsx deleted file mode 100644 index eeb954f037a..00000000000 --- a/apps/docs/components/Navigation/AlgoliaSearch.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { render } from 'react-dom' -import { IconCommand } from 'ui' -import { createElement, FC, useEffect, useRef, Fragment } from 'react' -import algoliasearch from 'algoliasearch/lite' -import { autocomplete, getAlgoliaResults } from '@algolia/autocomplete-js' -import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches' -import { useRouter } from 'next/router' - -// [Joshen] We're currently using DocSearch from Algolia as it provides a nice -// UI out of the box + some good preconfigured search settings (e.g hierarchy). -// However, we're using our own Algolia account to store the records in the indexes -// (rather than going through the DocSearch program from Algolia where they'll crawl -// our site for us). Refer to scripts/build-search on how we're saving the records. - -// Using Algolia's autocomplete library gives us full flexbility in terms of customizing -// our search experience, but that will take time to figure out. Hence why for now we're just -// using DocSearch with our own records. - -// Potentially for search, we could -// - Go ahead with the DocSearch program and let them crawl our site to generate the records -// - But we need to ensure that our site is semantically correct first -// - Use Algolia itself to flesh out our own search logic -// - The basics are already set up to be honest, but will take time to make it great -// - Go back to Typesense if we deem that Algolia is not helpful in the long run - -const searchClient = algoliasearch( - process.env.NEXT_PUBLIC_ALGOLIA_APP_ID, - process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY -) - -// [Joshen] Not working properly, but lets not get stuck on this -// Priority just to get a search working -const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({ - key: 'docs-search', - limit: 3, - //@ts-ignore - transformSource({ source }) { - return { - ...source, - templates: { - ...source.templates, - header({ state }) { - if (state.query) return null - return ( - - Your searches -
- - ) - }, - }, - } - }, -}) - -interface Props {} - -const AlgoliaSearch: FC = ({}) => { - const searchRef = useRef(null) - const router = useRouter() - - useEffect(() => { - if (!searchRef.current) { - return undefined - } - const search = autocomplete({ - openOnFocus: true, - container: searchRef.current, - defaultActiveItemId: 0, - detachedMediaQuery: '', - // @ts-ignore - renderer: { createElement, Fragment, render }, - placeholder: 'Search docs', - plugins: [recentSearchesPlugin], - renderNoResults({ state, render }, root) { - render( -
- No results found for {state.query}. -
, - root - ) - }, - navigator: { - navigate({ itemUrl }) { - router.push(itemUrl) - }, - }, - onSelect({ item, setQuery, setIsOpen, refresh }) { - //console.log('onSelect', item.url) - }, - // @ts-ignore - getSources({ query }) { - return [ - { - sourceId: 'pages', - templates: { - item({ item, components }) { - return ( - -
-
- {item.category && ( -

- <> - {item.category} - {item.version ? ` (${item.version})` : ''}: - -

- )} -

- -

-
-

{item.description as string}

-
-
- ) - }, - }, - getItemUrl({ item }) { - return item.url - }, - getItems() { - // if (!query) return [] - return getAlgoliaResults({ - searchClient, - queries: [ - { - indexName: 'dev_docs', - query, - }, - ], - }) - }, - }, - ] - }, - }) - - return () => { - search.destroy() - } - }, []) - - return ( -
-
-
-
- -
-
- K -
-
-
- ) -} - -export default AlgoliaSearch diff --git a/apps/docs/components/Navigation/NavBar.tsx b/apps/docs/components/Navigation/NavBar.tsx index e86cdb2802e..5096d3589b6 100644 --- a/apps/docs/components/Navigation/NavBar.tsx +++ b/apps/docs/components/Navigation/NavBar.tsx @@ -1,13 +1,12 @@ +import { useTheme } from 'common/Providers' import Image from 'next/image' import Link from 'next/link' import { useRouter } from 'next/router' -import { useState, useEffect, FC } from 'react' -import { IconMenu, IconMoon, IconSearch, IconSun, IconCommand, Listbox } from 'ui' -import { useTheme } from 'common/Providers' +import { FC, useEffect, useState } from 'react' +import { IconCommand, IconMenu, IconMoon, IconSearch, IconSun, Listbox, SearchButton } from 'ui' import { REFERENCES } from './Navigation.constants' import { getPageType } from '~/lib/helpers' -import SearchButton from '../Search/SearchButton' const NavBar: FC = () => { const { isDarkMode, toggleTheme } = useTheme() diff --git a/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx b/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx index 5e949d870e1..a3c1a9707e3 100644 --- a/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx @@ -1,13 +1,21 @@ +import { useTheme } from 'common/Providers' import Image from 'next/image' import Link from 'next/link' import { useRouter } from 'next/router' import { FC, useEffect, useState } from 'react' -import { Button, IconCommand, IconMenu, IconMoon, IconSearch, IconSun, Input, Listbox } from 'ui' +import { + Button, + IconCommand, + IconMenu, + IconMoon, + IconSearch, + IconSun, + Listbox, + SearchButton, +} from 'ui' import { REFERENCES } from '~/components/Navigation/Navigation.constants' -import { useTheme } from 'common/Providers' import { getPageType } from '~/lib/helpers' -import SearchButton from '~/components/Search/SearchButton' const TopNavBar: FC = () => { const { isDarkMode, toggleTheme } = useTheme() diff --git a/apps/docs/components/Navigation/NavigationMenu/TopNavBarRef.tsx b/apps/docs/components/Navigation/NavigationMenu/TopNavBarRef.tsx index baa7eaf6bc1..bb655bd3c6c 100644 --- a/apps/docs/components/Navigation/NavigationMenu/TopNavBarRef.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/TopNavBarRef.tsx @@ -3,10 +3,9 @@ import Image from 'next/image' import Link from 'next/link' import { useRouter } from 'next/router' import { FC, useEffect, useState } from 'react' -import { Button, IconCommand, IconGitHub, IconMoon, IconSearch, IconSun } from 'ui' +import { Button, IconCommand, IconGitHub, IconMoon, IconSearch, IconSun, SearchButton } from 'ui' import { REFERENCES } from '~/components/Navigation/Navigation.constants' -import SearchButton from '~/components/Search/SearchButton' import { getPageType } from '~/lib/helpers' const TopNavBarRef: FC = () => { diff --git a/apps/docs/components/Search/SearchButton.tsx b/apps/docs/components/Search/SearchButton.tsx deleted file mode 100644 index de86f28d0d8..00000000000 --- a/apps/docs/components/Search/SearchButton.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { ButtonHTMLAttributes, DetailedHTMLProps, FC, PropsWithChildren, useRef } from 'react' -import { useCommandMenu } from '~/../../packages/ui/src/components/Command/CommandMenuProvider' - -const SearchButton: FC< - PropsWithChildren, HTMLButtonElement>> -> = ({ children, ...props }) => { - const searchButtonRef = useRef() - const { setIsOpen } = useCommandMenu() - - return ( - - ) -} - -export default SearchButton diff --git a/apps/docs/components/Search/SearchModal.tsx b/apps/docs/components/Search/SearchModal.tsx deleted file mode 100644 index 213035540b1..00000000000 --- a/apps/docs/components/Search/SearchModal.tsx +++ /dev/null @@ -1,423 +0,0 @@ -import type { CreateCompletionResponse } from 'openai' -import { FC, useCallback, useRef, useState } from 'react' -import ReactMarkdown from 'react-markdown' -import remarkGfm from 'remark-gfm' -import { SSE } from 'sse.js' -import clippyImageDark from '../../public/img/clippy-dark.png' -import clippyImage from '../../public/img/clippy.png' - -import { useSupabaseClient } from '@supabase/auth-helpers-react' -import { useTheme } from 'common/Providers' -import Image from 'next/image' -import { - Button, - IconAlertCircle, - IconAlertTriangle, - IconLoader, - IconSearch, - Input, - Loading, - Modal, - Tabs, -} from 'ui' -import components from '~/components' -import { IS_PLATFORM } from '~/lib/constants' -import { useSearch } from './SearchProvider' -import SearchResult, { SearchResultType } from './SearchResult' - -const questions = [ - 'How do I get started with Supabase?', - 'How do I run Supabase locally?', - 'How do I connect to my database?', - 'How do I run migrations? ', - 'How do I listen to changes in a table?', - 'How do I set up authentication?', -] - -function getEdgeFunctionUrl() { - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL?.replace(/\/$/, '') - - if (IS_PLATFORM) { - const [schemeAndProjectId, domain, tld] = supabaseUrl.split('.') - return `${schemeAndProjectId}.functions.${domain}.${tld}` - } else { - return `${supabaseUrl}/functions/v1` - } -} - -const edgeFunctionUrl = getEdgeFunctionUrl() - -const SearchModal: FC = () => { - const { isDarkMode } = useTheme() - const { close, query, setQuery } = useSearch() - const [answer, setAnswer] = useState('') - const [results, setResults] = useState() - const [isLoading, setIsLoading] = useState(false) - const [isResponding, setIsResponding] = useState(false) - const [hasClippyError, setHasClippyError] = useState(false) - const [hasSearchError, setHasSearchError] = useState(false) - const [selectedTab, setSelectedTab] = useState('search-panel') - const eventSourceRef = useRef() - const supabaseClient = useSupabaseClient() - - const cantHelp = answer?.trim() === "Sorry, I don't know how to help with that." - const status = isLoading - ? 'Clippy is searching...' - : isResponding - ? 'Clippy is responding...' - : cantHelp || hasClippyError - ? 'Clippy has failed you' - : undefined - - const handleSearchConfirm = useCallback( - async (query: string) => { - setResults(undefined) - setAnswer(undefined) - setIsResponding(false) - setHasClippyError(false) - setHasSearchError(false) - setIsLoading(true) - - const { error, data: pageSections } = await supabaseClient.functions.invoke('search', { - body: { query }, - }) - - setIsLoading(false) - - if (error) { - setIsLoading(false) - setIsResponding(false) - setHasSearchError(true) - console.error(error) - return - } - - if (!Array.isArray(pageSections)) { - setIsLoading(false) - setIsResponding(false) - setHasSearchError(true) - console.error('Malformed response') - return - } - - setResults(pageSections) - }, - [supabaseClient] - ) - - const handleClippyConfirm = useCallback(async (query: string) => { - setResults(undefined) - setAnswer(undefined) - setIsResponding(false) - setHasClippyError(false) - setHasSearchError(false) - setIsLoading(true) - - const eventSource = new SSE(`${edgeFunctionUrl}/clippy-search`, { - headers: { - apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, - Authorization: `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}`, - 'Content-Type': 'application/json', - }, - payload: JSON.stringify({ query }), - }) - - function handleError(err: T) { - setIsLoading(false) - setIsResponding(false) - setHasClippyError(true) - console.error(err) - } - - eventSource.addEventListener('error', handleError) - eventSource.addEventListener('message', (e) => { - try { - setIsLoading(false) - - if (e.data === '[DONE]') { - setIsResponding(false) - return - } - - setIsResponding(true) - - const completionResponse: CreateCompletionResponse = JSON.parse(e.data) - const [{ text }] = completionResponse.choices - - setAnswer((answer) => { - return (answer ?? '') + text - }) - } catch (err) { - handleError(err) - } - }) - - eventSource.stream() - - eventSourceRef.current = eventSource - - setIsLoading(true) - }, []) - - const handleConfirm = useCallback( - (selectedTab: string, query: string) => { - switch (selectedTab) { - case 'search-panel': - return handleSearchConfirm(query) - case 'clippy-panel': - return handleClippyConfirm(query) - } - }, - [handleSearchConfirm, handleClippyConfirm] - ) - - function handleResetPrompt() { - eventSourceRef.current?.close() - eventSourceRef.current = undefined - setQuery('') - setResults(undefined) - setAnswer(undefined) - setIsResponding(false) - setHasClippyError(false) - setHasSearchError(false) - } - return ( - -
e.stopPropagation()} - > -
- setQuery(e.target.value)} - icon={} - onKeyDown={(e) => { - switch (e.key) { - case 'Enter': - if (!query) { - return - } - handleConfirm(selectedTab, query) - return - default: - return - } - }} - /> -
- -
- {!isLoading && answer && ( -
- -
- )} -
- { - setSelectedTab(tabId) - if (!query) { - handleResetPrompt() - return - } - handleConfirm(tabId, query) - }} - > - -
- {!isLoading && !hasSearchError && !results && ( -
-

- Search Supabase guides & reference docs -

-
- )} - {results && results.length > 0 && ( -
- {results.map((page) => { - const pageSections = page.sections.filter((section) => !!section.heading) - return ( -
- - {pageSections.length > 0 && ( -
-
-
- {pageSections.map((section) => ( - - ))} -
-
- )} -
- ) - })} -
- )} - {isLoading && ( -
- {} -

Searching for results

-
- )} - {results && results.length === 0 && ( -
- -

No results found.

- -
- )} - {hasSearchError && ( -
- -

- Sorry, looks like we're having some issues with search! -

-

Please try again in a bit.

- -
- )} -
-
- - {!isLoading && !answer && !hasClippyError && ( -
-
-

Not sure where to start?

- -
    - {questions.map((question) => { - const key = question.replace(/\s+/g, '_') - return ( -
  • - -
  • - ) - })} -
-
-
- )} - {answer && ( -
- {cantHelp ? ( -

-

- -

Sorry, I don't know how to help with that.

-
- -

- ) : ( -
- { - const supabaseUrl = new URL('https://supabase.com') - const linkUrl = new URL(href, 'https://supabase.com') - - if (linkUrl.origin === supabaseUrl.origin) { - return linkUrl.toString() - } - - return href - }} - components={components} - > - {answer} - -
- )} -
- )} - {isLoading && ( -
- {} -

Searching for results

-
- )} - {hasClippyError && ( -
- -

- Sorry, looks like Clippy is having a hard time! -

-

Please try again in a bit.

- -
- )} -
-
-
- Powered by OpenAI. - - Read the blog post - -
-
- {status ? ( - - {(isLoading || isResponding) && ( - - )} - {status} - - ) : ( - <> - )} - Clippy -
-
-
-
-
-
-
- ) -} - -export default SearchModal diff --git a/apps/docs/components/Search/SearchProvider.tsx b/apps/docs/components/Search/SearchProvider.tsx deleted file mode 100644 index 4175c159c29..00000000000 --- a/apps/docs/components/Search/SearchProvider.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { - createContext, - FC, - PropsWithChildren, - useCallback, - useContext, - useEffect, - useState, -} from 'react' -import { createPortal } from 'react-dom' -import SearchModal from './SearchModal' - -export type SearchContextValue = { - isOpen: boolean - open: () => void - close: () => void - query: string - setQuery: (query: string) => void -} - -export const SearchContext = createContext(null) - -export const useSearch = () => { - const { isOpen, open, close, query, setQuery } = useContext(SearchContext) - - return { isOpen, open, close, query, setQuery } -} - -const SearchProvider: FC> = ({ children }) => { - const [isOpen, setIsOpen] = useState(false) - const [query, setQuery] = useState('') - - const open = useCallback(() => { - setIsOpen(true) - document.body.classList.add('DocSearch--active') - }, []) - - const close = useCallback(() => { - setIsOpen(false) - document.body.classList.remove('DocSearch--active') - }, []) - - useSearchKeyboardEvents({ - open, - close, - }) - - return ( - - {children} - {isOpen && createPortal(, document.body)} - - ) -} - -function useSearchKeyboardEvents({ open, close }) { - useEffect(() => { - function onKeyDown(event: KeyboardEvent) { - switch (event.key) { - case 'Escape': - close() - return - case 'k': - case '/': - if (event.metaKey || event.ctrlKey) { - open() - } - return - } - } - - window.addEventListener('keydown', onKeyDown) - - return () => { - window.removeEventListener('keydown', onKeyDown) - } - }, [open, close]) -} - -export default SearchProvider diff --git a/apps/docs/components/Search/SearchResult.tsx b/apps/docs/components/Search/SearchResult.tsx deleted file mode 100644 index d3689024ec5..00000000000 --- a/apps/docs/components/Search/SearchResult.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import Link from 'next/link' -import { FC } from 'react' -import { IconBookOpen, IconHash } from '~/../../packages/ui' -import { useSearch } from './SearchProvider' - -export enum SearchResultType { - Document = 'document', - Section = 'section', -} - -type Props = { - href: string - type: SearchResultType - title: string - chip?: string -} - -const SearchResult: FC = ({ href, type, title, chip }) => { - const { close } = useSearch() - - return ( - - -
- {getIconByType(type)} -
-
- {chip && ( -
- {chip} -
- )} -
{title}
-
-
- - ) -} - -function getIconByType(type: SearchResultType) { - switch (type) { - case SearchResultType.Document: - return - case SearchResultType.Section: - return - default: - throw new Error(`Unknown search result type '${type}'`) - } -} - -export default SearchResult diff --git a/packages/ui/src/components/Command/AiCommand.tsx b/packages/ui/src/components/Command/AiCommand.tsx index e2e9c42e238..993ff42a773 100644 --- a/packages/ui/src/components/Command/AiCommand.tsx +++ b/packages/ui/src/components/Command/AiCommand.tsx @@ -25,8 +25,6 @@ import { } from 'ui' // import components from '~/components' // import { IS_PLATFORM } from '~/lib/constants' -// import { SearchContextValue } from './SearchProvider' -import SearchResult, { SearchResultType } from './SearchResult' import { CommandGroup, CommandItem, CommandInput } from './Command.utils' import { IconCopy } from '../Icon/icons/IconCopy' diff --git a/packages/ui/src/components/Command/Command.tsx b/packages/ui/src/components/Command/Command.tsx index 121b3b55a33..1ff973f7950 100644 --- a/packages/ui/src/components/Command/Command.tsx +++ b/packages/ui/src/components/Command/Command.tsx @@ -27,7 +27,6 @@ import { IconMoon } from '../Icon/icons/IconMoon' import { IconCopy } from '../Icon/icons/IconCopy' import DocsSearch from './DocsSearch' import { useCommandMenu } from './CommandMenuProvider' -// import { SearchProvider } from './SearchProvider' export const AiIcon = () => ( { {pageSections.length > 0 && (
{pageSections.map((section, i) => ( - // , HTMLButtonElement> +>) => { + const searchButtonRef = useRef() + const { setIsOpen } = useCommandMenu() + + return ( + + ) +} + +export default SearchButton diff --git a/packages/ui/src/components/Command/SearchProvider.tsx b/packages/ui/src/components/Command/SearchProvider.tsx deleted file mode 100644 index 48229bad589..00000000000 --- a/packages/ui/src/components/Command/SearchProvider.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React, { FC, PropsWithChildren } from 'react' -import { AiCommand } from './AiCommand' - -const SearchProvider: FC = () => { - return -} - -export { SearchProvider } diff --git a/packages/ui/src/components/Command/SearchResult.tsx b/packages/ui/src/components/Command/SearchResult.tsx deleted file mode 100644 index 091cb0f31e3..00000000000 --- a/packages/ui/src/components/Command/SearchResult.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react' - -import Link from 'next/link' -import { FC } from 'react' -import { IconBookOpen, IconHash } from '~/../../packages/ui' -// import { useSearch } from './SearchProvider' - -export enum SearchResultType { - Document = 'document', - Section = 'section', -} - -type Props = { - href: string - type: SearchResultType - title: string - chip?: string -} - -const SearchResult: FC = ({ href, type, title, chip }) => { - // const { close } = useSearch() - - return ( - // - -
- {getIconByType(type)} -
-
- {chip && ( -
- {chip} -
- )} -
{title}
-
-
- // - ) -} - -function getIconByType(type: SearchResultType) { - switch (type) { - case SearchResultType.Document: - return - case SearchResultType.Section: - return - default: - throw new Error(`Unknown search result type '${type}'`) - } -} - -export default SearchResult diff --git a/packages/ui/src/components/Command/index.tsx b/packages/ui/src/components/Command/index.tsx index 4d57b6070e3..f0302798aee 100644 --- a/packages/ui/src/components/Command/index.tsx +++ b/packages/ui/src/components/Command/index.tsx @@ -2,3 +2,5 @@ export * from './Command' export { default as CommandMenuProvider } from './CommandMenuProvider' export * from './CommandMenuProvider' + +export { default as SearchButton } from './SearchButton'