mirror of
https://github.com/supabase/supabase.git
synced 2026-06-20 14:26:06 +08:00
refactor(cmdk): remove old docs search
This commit is contained in:
@@ -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 (
|
||||
<Fragment>
|
||||
<span className="aa-SourceHeaderTitle">Your searches</span>
|
||||
<div className="aa-SourceHeaderLine" />
|
||||
</Fragment>
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
interface Props {}
|
||||
|
||||
const AlgoliaSearch: FC<Props> = ({}) => {
|
||||
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(
|
||||
<div className="text-scale-1100 py-2 text-sm px-4">
|
||||
No results found for {state.query}.
|
||||
</div>,
|
||||
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 (
|
||||
<a
|
||||
href={item.url as string}
|
||||
className="aa-ItemLink truncate flex justify-between space-x-4"
|
||||
>
|
||||
<div className="aa-ItemContent w-full">
|
||||
<div className="aa-ItemTitle flex items-center space-x-1">
|
||||
{item.category && (
|
||||
<p
|
||||
className={`${
|
||||
['cli', 'api'].includes(item.category as string)
|
||||
? 'uppercase'
|
||||
: 'capitalize'
|
||||
}`}
|
||||
>
|
||||
<>
|
||||
{item.category}
|
||||
{item.version ? ` (${item.version})` : ''}:
|
||||
</>
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<components.Highlight hit={item} attribute="title" />
|
||||
</p>
|
||||
</div>
|
||||
<p className="aa-ItemContentSubtitle">{item.description as string}</p>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
},
|
||||
},
|
||||
getItemUrl({ item }) {
|
||||
return item.url
|
||||
},
|
||||
getItems() {
|
||||
// if (!query) return []
|
||||
return getAlgoliaResults({
|
||||
searchClient,
|
||||
queries: [
|
||||
{
|
||||
indexName: 'dev_docs',
|
||||
query,
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
})
|
||||
|
||||
return () => {
|
||||
search.destroy()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="w-[200px] relative">
|
||||
<div ref={searchRef} />
|
||||
<div className="flex items-center space-x-1 absolute top-[7px] right-2">
|
||||
<div className="text-scale-1200 flex items-center justify-center h-6 w-6 rounded bg-scale-500">
|
||||
<IconCommand size={12} strokeWidth={1.5} />
|
||||
</div>
|
||||
<div className="text-xs text-scale-1200 flex items-center justify-center h-6 w-6 rounded bg-scale-500">
|
||||
K
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AlgoliaSearch
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>>
|
||||
> = ({ children, ...props }) => {
|
||||
const searchButtonRef = useRef<HTMLButtonElement>()
|
||||
const { setIsOpen } = useCommandMenu()
|
||||
|
||||
return (
|
||||
<button type="button" ref={searchButtonRef} onClick={() => setIsOpen(true)} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchButton
|
||||
@@ -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<any[]>()
|
||||
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<SSE>()
|
||||
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<T>(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 (
|
||||
<Modal size="xlarge" visible={true} onCancel={close} closable={false} hideFooter>
|
||||
<div
|
||||
className={`mx-auto max-h-[90vh] lg:max-h-[75vh] flex flex-col gap-4 rounded-lg p-4 md:pt-6 md:px-6 pb-2 w-full shadow-2xl overflow-hidden border text-left border-scale-500 bg-scale-100 dark:bg-scale-300 cursor-auto relative min-w-[340px]`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="w-full"
|
||||
size="xlarge"
|
||||
autoFocus
|
||||
placeholder={selectedTab === 'search-panel' ? 'Search documentation' : 'Ask a question'}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
icon={<IconSearch size="small" />}
|
||||
onKeyDown={(e) => {
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
if (!query) {
|
||||
return
|
||||
}
|
||||
handleConfirm(selectedTab, query)
|
||||
return
|
||||
default:
|
||||
return
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="absolute right-0 top-0 mt-3 mr-4 hidden md:block">
|
||||
<Button type="default" size="tiny" onClick={close}>
|
||||
esc
|
||||
</Button>
|
||||
</div>
|
||||
{!isLoading && answer && (
|
||||
<div className="absolute right-0 top-0 mt-3 mr-16 hidden md:block">
|
||||
<Button type="text" size="tiny" onClick={handleResetPrompt}>
|
||||
Try again
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Tabs
|
||||
activeId={selectedTab}
|
||||
onChange={(tabId) => {
|
||||
setSelectedTab(tabId)
|
||||
if (!query) {
|
||||
handleResetPrompt()
|
||||
return
|
||||
}
|
||||
handleConfirm(tabId, query)
|
||||
}}
|
||||
>
|
||||
<Tabs.Panel id="search-panel" label="Guides & Reference">
|
||||
<div className="mb-6">
|
||||
{!isLoading && !hasSearchError && !results && (
|
||||
<div className="p-10 grid">
|
||||
<h2 className="text-lg text-center text-scale-1100">
|
||||
Search Supabase guides & reference docs
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
{results && results.length > 0 && (
|
||||
<div className="flex flex-col gap-3 max-h-[70vh] lg:max-h-[50vh] overflow-y-auto px-4 py-4 rounded-lg bg-scale-200">
|
||||
{results.map((page) => {
|
||||
const pageSections = page.sections.filter((section) => !!section.heading)
|
||||
return (
|
||||
<div key={page.id} className="flex flex-col gap-3">
|
||||
<SearchResult
|
||||
href={page.path}
|
||||
type={SearchResultType.Document}
|
||||
title={page.meta.title}
|
||||
/>
|
||||
{pageSections.length > 0 && (
|
||||
<div className="flex flex-row">
|
||||
<div className="border bg-scale-300 rounded-xl self-stretch p-[1px] ml-4 mr-4"></div>
|
||||
<div className="flex flex-col gap-3 items-stretch grow">
|
||||
{pageSections.map((section) => (
|
||||
<SearchResult
|
||||
key={section.id}
|
||||
href={`${page.path}${page.type === 'reference' ? '/' : '#'}${
|
||||
section.slug
|
||||
}`}
|
||||
type={SearchResultType.Section}
|
||||
title={section.heading}
|
||||
chip={page.meta.title}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="p-6 grid gap-6 mt-4">
|
||||
<Loading active>{}</Loading>
|
||||
<p className="text-lg text-center">Searching for results</p>
|
||||
</div>
|
||||
)}
|
||||
{results && results.length === 0 && (
|
||||
<div className="p-6 flex flex-col items-center gap-6 mt-4">
|
||||
<IconAlertTriangle strokeWidth={1.5} size={40} />
|
||||
<p className="text-lg text-center">No results found.</p>
|
||||
<Button size="tiny" type="secondary" onClick={handleResetPrompt}>
|
||||
Try again?
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{hasSearchError && (
|
||||
<div className="p-6 flex flex-col items-center gap-6 mt-4">
|
||||
<IconAlertTriangle strokeWidth={1.5} size={40} />
|
||||
<p className="text-lg text-center">
|
||||
Sorry, looks like we're having some issues with search!
|
||||
</p>
|
||||
<p className="text-sm text-center">Please try again in a bit.</p>
|
||||
<Button size="tiny" type="secondary" onClick={handleResetPrompt}>
|
||||
Try again?
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel id="clippy-panel" label="Ask Clippy">
|
||||
{!isLoading && !answer && !hasClippyError && (
|
||||
<div className="">
|
||||
<div className="mt-2">
|
||||
<h2 className="text-sm text-scale-1100">Not sure where to start?</h2>
|
||||
|
||||
<ul className="text-sm mt-4 text-scale-1100 grid md:flex gap-4 flex-wrap max-w-3xl">
|
||||
{questions.map((question) => {
|
||||
const key = question.replace(/\s+/g, '_')
|
||||
return (
|
||||
<li key={key}>
|
||||
<button
|
||||
className="hover:bg-slate-400 hover:dark:bg-slate-400 px-4 py-2 bg-slate-300 dark:bg-slate-200 rounded-lg transition-colors"
|
||||
onClick={() => {
|
||||
setQuery(question)
|
||||
handleClippyConfirm(question)
|
||||
}}
|
||||
>
|
||||
{question}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{answer && (
|
||||
<div className="px-4 py-4 rounded-lg overflow-y-auto bg-scale-200 max-h-[70vh] lg:max-h-[50vh]">
|
||||
{cantHelp ? (
|
||||
<p className="flex flex-col gap-4 items-center p-4">
|
||||
<div className="grid md:flex items-center gap-2 mt-4 text-center justify-items-center">
|
||||
<IconAlertCircle />
|
||||
<p>Sorry, I don't know how to help with that.</p>
|
||||
</div>
|
||||
<Button size="tiny" type="secondary" onClick={handleResetPrompt}>
|
||||
Try again?
|
||||
</Button>
|
||||
</p>
|
||||
) : (
|
||||
<div className="prose dark:prose-dark">
|
||||
<ReactMarkdown
|
||||
linkTarget="_blank"
|
||||
remarkPlugins={[remarkGfm]}
|
||||
transformLinkUri={(href) => {
|
||||
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}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="p-6 grid gap-6 mt-4">
|
||||
<Loading active>{}</Loading>
|
||||
<p className="text-lg text-center">Searching for results</p>
|
||||
</div>
|
||||
)}
|
||||
{hasClippyError && (
|
||||
<div className="p-6 flex flex-col items-center gap-6 mt-4">
|
||||
<IconAlertTriangle strokeWidth={1.5} size={40} />
|
||||
<p className="text-lg text-center">
|
||||
Sorry, looks like Clippy is having a hard time!
|
||||
</p>
|
||||
<p className="text-sm text-center">Please try again in a bit.</p>
|
||||
<Button size="tiny" type="secondary" onClick={handleResetPrompt}>
|
||||
Try again?
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t border-scale-600 mt-4 text-scale-1100">
|
||||
<div className="flex justify-between items-center py-2 text-xs">
|
||||
<div className="flex items-centerp gap-1 pt-3 pb-1">
|
||||
<span>Powered by OpenAI.</span>
|
||||
<a href="/blog/chatgpt-supabase-docs" className="underline">
|
||||
Read the blog post
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 py-1">
|
||||
{status ? (
|
||||
<span className="bg-scale-400 rounded-lg py-1 px-2 items-center gap-2 hidden md:flex">
|
||||
{(isLoading || isResponding) && (
|
||||
<IconLoader size={14} className="animate-spin" />
|
||||
)}
|
||||
{status}
|
||||
</span>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Image
|
||||
width={30}
|
||||
height={34}
|
||||
src={isDarkMode ? clippyImageDark : clippyImage}
|
||||
alt="Clippy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchModal
|
||||
@@ -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<SearchContextValue>(null)
|
||||
|
||||
export const useSearch = () => {
|
||||
const { isOpen, open, close, query, setQuery } = useContext(SearchContext)
|
||||
|
||||
return { isOpen, open, close, query, setQuery }
|
||||
}
|
||||
|
||||
const SearchProvider: FC<PropsWithChildren<{}>> = ({ 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 (
|
||||
<SearchContext.Provider value={{ isOpen, open, close, query, setQuery }}>
|
||||
{children}
|
||||
{isOpen && createPortal(<SearchModal />, document.body)}
|
||||
</SearchContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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<Props> = ({ href, type, title, chip }) => {
|
||||
const { close } = useSearch()
|
||||
|
||||
return (
|
||||
<Link href={href}>
|
||||
<a
|
||||
className="flex flex-row items-center bg-scale-400 hover:bg-scale-600 transition p-4 rounded-md border border-scale-600 text-sm cursor-pointer"
|
||||
onClick={close}
|
||||
>
|
||||
<div className="w-6 h-6 p-1 flex items-center justify-center mr-4 text-brand-1100 rounded-md bg-scale-700">
|
||||
{getIconByType(type)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 items-start">
|
||||
{chip && (
|
||||
<div className="rounded-xl bg-scale-700 pl-3 pr-3 pt-0.5 pb-0.5 text-xs text-scale-1100">
|
||||
{chip}
|
||||
</div>
|
||||
)}
|
||||
<div>{title}</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
function getIconByType(type: SearchResultType) {
|
||||
switch (type) {
|
||||
case SearchResultType.Document:
|
||||
return <IconBookOpen />
|
||||
case SearchResultType.Section:
|
||||
return <IconHash />
|
||||
default:
|
||||
throw new Error(`Unknown search result type '${type}'`)
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchResult
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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 = () => (
|
||||
<svg
|
||||
|
||||
@@ -295,15 +295,6 @@ const DocsSearch = ({ query, setQuery, page, router }: DocsSearchProps) => {
|
||||
{pageSections.length > 0 && (
|
||||
<div className="border-l border-scale-500 ml-3 pt-3">
|
||||
{pageSections.map((section, i) => (
|
||||
// <SearchResult
|
||||
// key={section.id}
|
||||
// href={`${page.path}${page.type === 'reference' ? '/' : '#'}${
|
||||
// section.slug
|
||||
// }`}
|
||||
// type={SearchResultType.Section}
|
||||
// title={section.heading}
|
||||
// chip={page.meta.title}
|
||||
// />
|
||||
<CommandItem
|
||||
forceMount
|
||||
className="ml-3 mb-3"
|
||||
|
||||
21
packages/ui/src/components/Command/SearchButton.tsx
Normal file
21
packages/ui/src/components/Command/SearchButton.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from 'react'
|
||||
import { ButtonHTMLAttributes, DetailedHTMLProps, PropsWithChildren, useRef } from 'react'
|
||||
import { useCommandMenu } from './CommandMenuProvider'
|
||||
|
||||
const SearchButton = ({
|
||||
children,
|
||||
...props
|
||||
}: PropsWithChildren<
|
||||
DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
|
||||
>) => {
|
||||
const searchButtonRef = useRef<HTMLButtonElement>()
|
||||
const { setIsOpen } = useCommandMenu()
|
||||
|
||||
return (
|
||||
<button type="button" ref={searchButtonRef} onClick={() => setIsOpen(true)} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchButton
|
||||
@@ -1,8 +0,0 @@
|
||||
import React, { FC, PropsWithChildren } from 'react'
|
||||
import { AiCommand } from './AiCommand'
|
||||
|
||||
const SearchProvider: FC = () => {
|
||||
return <AiCommand />
|
||||
}
|
||||
|
||||
export { SearchProvider }
|
||||
@@ -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<Props> = ({ href, type, title, chip }) => {
|
||||
// const { close } = useSearch()
|
||||
|
||||
return (
|
||||
// <Link href={href}>
|
||||
<a
|
||||
className="flex flex-row items-center bg-scale-400 hover:bg-scale-600 transition p-4 rounded-md border border-scale-600 text-sm cursor-pointer"
|
||||
onClick={close}
|
||||
>
|
||||
<div className="w-6 h-6 p-1 flex items-center justify-center mr-4 text-brand-1100 rounded-md bg-scale-700">
|
||||
{getIconByType(type)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 items-start">
|
||||
{chip && (
|
||||
<div className="rounded-xl bg-scale-700 pl-3 pr-3 pt-0.5 pb-0.5 text-xs text-scale-1100">
|
||||
{chip}
|
||||
</div>
|
||||
)}
|
||||
<div>{title}</div>
|
||||
</div>
|
||||
</a>
|
||||
// </Link>
|
||||
)
|
||||
}
|
||||
|
||||
function getIconByType(type: SearchResultType) {
|
||||
switch (type) {
|
||||
case SearchResultType.Document:
|
||||
return <IconBookOpen />
|
||||
case SearchResultType.Section:
|
||||
return <IconHash />
|
||||
default:
|
||||
throw new Error(`Unknown search result type '${type}'`)
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchResult
|
||||
@@ -2,3 +2,5 @@ export * from './Command'
|
||||
|
||||
export { default as CommandMenuProvider } from './CommandMenuProvider'
|
||||
export * from './CommandMenuProvider'
|
||||
|
||||
export { default as SearchButton } from './SearchButton'
|
||||
|
||||
Reference in New Issue
Block a user