Files
supabase/packages/ui/src/components/Command/CommandMenu.tsx
T
Charis ba3ba81434 feat: add full text search to docs (#19112)
Feature

Adds a full-text search endpoint for docs search, to provide more comprehensive and faster results. Each docs search now performs both FTS and embeddings-based search, and displays results as soon as the first set of results is returned.

Refactor

Slight refactor of the embeddings search to make only one trip to the database, and transfer less content in the JSON. This now uses a different function than the RAG step for Supabase AI.

Bug fix

Fixes a bug where some search results were being filtered out by the cmdk menu.

Removes an obsolete guide. Content is now elsewhere and it messes up search results.
2023-11-27 14:32:22 -05:00

315 lines
11 KiB
TypeScript

import { useRouter } from 'next/router'
import * as React from 'react'
import { ElementRef, useRef } from 'react'
import { IconHome } from '../Icon/icons/IconHome'
import { IconArrowRight } from './../Icon/icons/IconArrowRight'
import { IconBook } from './../Icon/icons/IconBook'
import { IconColumns } from './../Icon/icons/IconColumns'
import { IconInbox } from './../Icon/icons/IconInbox'
import { IconLifeBuoy } from './../Icon/icons/IconLifeBuoy'
import { IconMonitor } from './../Icon/icons/IconMonitor'
import { IconPhone } from './../Icon/icons/IconPhone'
import { IconUser } from './../Icon/icons/IconUser'
import { IconKey } from './../Icon/icons/IconKey'
import { IconLink } from './../Icon/icons/IconLink'
import AiCommand from './AiCommand'
import sharedItems from './utils/shared-nav-items.json'
import { AiIcon } from './Command.icons'
import {
CommandDialog,
CommandGroup,
CommandInput,
CommandItem,
CommandLabel,
CommandList,
copyToClipboard,
} from './Command.utils'
import { COMMAND_ROUTES } from './Command.constants'
import { useCommandMenu } from './CommandMenuProvider'
import DocsSearch from './DocsSearch'
import GenerateSQL from './GenerateSQL'
import ThemeOptions from './ThemeOptions'
import APIKeys from './APIKeys'
import SearchableStudioItems from './SearchableStudioItems'
import CommandMenuShortcuts from './CommandMenuShortcuts'
import { BadgeExperimental } from './Command.Badges'
import { AiIconAnimation } from '@ui/layout/ai-icon-animation'
import ChildItem from './ChildItem'
export const CHAT_ROUTES = [
COMMAND_ROUTES.AI, // this one is temporary
COMMAND_ROUTES.AI_ASK_ANYTHING,
COMMAND_ROUTES.AI_RLS_POLICY,
COMMAND_ROUTES.GENERATE_SQL,
]
const iconPicker: { [key: string]: React.ReactNode } = {
arrowRight: <IconArrowRight />,
book: <IconBook />,
inbox: <IconInbox />,
mobile: <IconPhone />,
person: <IconUser />,
services: <IconColumns />,
contact: <IconMonitor />,
icon: <IconHome />,
products: <IconColumns />,
}
interface CommandMenuProps {
projectRef?: string
}
const CommandMenu = ({ projectRef }: CommandMenuProps) => {
const router = useRouter()
const {
isOpen,
setIsOpen,
search,
setSearch,
pages,
setPages,
currentPage,
site,
project,
inputRef: commandInputRef,
} = useCommandMenu()
const showCommandInput = !currentPage || !CHAT_ROUTES.includes(currentPage)
// This function has been added to prevent the use of double quotes in the search docs input due to an issue with the cmdk-supabase module. This function can be removed when we transition to using cmdk.
const handleInputChange = (value: string) => {
const newValue = value.replace(/"/g, '') // Remove double quotes
setSearch(newValue)
}
return (
<>
<CommandDialog
page={currentPage}
visible={isOpen}
onInteractOutside={(e) => {
// Only hide menu when clicking outside, not focusing outside
// Prevents Firefox dropdown issue that immediately closes menu after opening
if (e.type === 'dismissableLayer.pointerDownOutside') {
setIsOpen(!open)
}
}}
size={'xlarge'}
className={'max-h-[70vh] lg:max-h-[50vh] overflow-hidden overflow-y-auto'}
>
{pages.length > 0 && <CommandMenuShortcuts />}
{showCommandInput && (
<CommandInput
ref={commandInputRef}
placeholder="Type a command or search..."
value={search}
onValueChange={handleInputChange}
/>
)}
<CommandList className={['my-2', showCommandInput && 'max-h-[300px]'].join(' ')}>
{!currentPage && (
<>
<CommandGroup heading="Documentation">
<CommandItem
type="command"
onSelect={() => setPages([...pages, COMMAND_ROUTES.DOCS_SEARCH])}
>
<IconBook className="" />
<span>
Search the docs
{search ? (
<>
{': '}
<span className="text-foreground font-semibold">{search}</span>
</>
) : (
'...'
)}
</span>
</CommandItem>
<CommandItem
type="command"
badge={<BadgeExperimental />}
onSelect={() => {
setPages([...pages, COMMAND_ROUTES.AI])
}}
>
<AiIconAnimation />
<span className="text-brand">
Ask Supabase AI
{search ? (
<>
{': '}
<span className="text-foreground font-semibold">{search}</span>
</>
) : (
'...'
)}
</span>
</CommandItem>
</CommandGroup>
{site === 'docs' && (
<CommandGroup heading="Quickstarts">
{sharedItems.quickstarts.map((item) => (
<CommandItem key={item.url} type="link" onSelect={() => router.push(item.url)}>
<IconArrowRight className="text-foreground-muted" />
<CommandLabel>
Start with <span className="font-bold"> {item.label}</span>
</CommandLabel>
</CommandItem>
))}
</CommandGroup>
)}
{site === 'docs' && (
<CommandGroup heading="Projects">
{sharedItems.projectTools.map((item) => (
<CommandItem key={item.url} type="link" onSelect={() => router.push(item.url)}>
<IconArrowRight className="text-foreground-muted" />
<CommandLabel>
<span className="font-bold"> {item.label}</span>
</CommandLabel>
</CommandItem>
))}
</CommandGroup>
)}
{site === 'docs' && (
<CommandGroup heading="Studio tools">
{sharedItems.tools.map((item) => (
<CommandItem key={item.url} type="link" onSelect={() => router.push(item.url)}>
<IconArrowRight className="text-foreground-muted" />
<CommandLabel>
Go to <span className="font-bold"> {item.label}</span>
</CommandLabel>
</CommandItem>
))}
</CommandGroup>
)}
{site === 'studio' && (
<CommandGroup heading="Experimental">
<CommandItem
type="command"
badge={<BadgeExperimental />}
onSelect={() => setPages([...pages, COMMAND_ROUTES.GENERATE_SQL])}
>
<AiIcon className="text-foreground-light" />
<CommandLabel>Generate SQL with Supabase AI</CommandLabel>
</CommandItem>
</CommandGroup>
)}
{site === 'studio' && projectRef !== undefined && (
<CommandGroup heading="Project tools">
<CommandItem
type="command"
onSelect={() => {
setSearch('')
setPages([...pages, COMMAND_ROUTES.API_KEYS])
}}
>
<IconKey className="text-foreground-light" />
<CommandLabel>Get API keys</CommandLabel>
</CommandItem>
{project?.apiUrl !== undefined && (
<ChildItem
isSubItem={false}
onSelect={() => {
copyToClipboard(project?.apiUrl ?? '')
setIsOpen(false)
}}
className="space-x-2"
>
<IconLink className="text-foreground-light" />
<CommandLabel>Copy API URL</CommandLabel>
</ChildItem>
)}
</CommandGroup>
)}
{site === 'studio' && (
<CommandGroup heading="Navigate">
{sharedItems.tools.map((item) => {
const itemUrl = (
projectRef ? item.url.replace('_', projectRef) : item.url
).split('https://supabase.com/dashboard')[1]
return (
<CommandItem
key={item.url}
type="link"
onSelect={() => {
router.push(itemUrl)
setIsOpen(false)
}}
>
<IconArrowRight className="text-foreground-muted" />
<CommandLabel>
Go to <span className="font-bold"> {item.label}</span>
</CommandLabel>
</CommandItem>
)
})}
</CommandGroup>
)}
{/* <DashboardTableEditor /> */}
<CommandGroup heading="Support">
{sharedItems.support.map((item) => (
<CommandItem key={item.url} type="link" onSelect={() => router.push(item.url)}>
<IconLifeBuoy className="text-foreground-muted" />
<CommandLabel>
Go to <span className="font-bold"> {item.label}</span>
</CommandLabel>
</CommandItem>
))}
</CommandGroup>
{site === 'docs' && (
<CommandGroup heading="General">
{sharedItems.docsGeneral.map((item) => (
<CommandItem key={item.url} type="link" onSelect={() => router.push(item.url)}>
{item?.icon && iconPicker[item.icon]}
<CommandLabel>{item.label}</CommandLabel>
</CommandItem>
))}
</CommandGroup>
)}
<CommandGroup heading="Settings">
<CommandItem
type="link"
onSelect={() => {
setSearch('')
setPages([...pages, 'Theme'])
}}
>
<IconMonitor />
Change theme
</CommandItem>
</CommandGroup>
<ThemeOptions isSubItem />
{site === 'studio' && search && <SearchableStudioItems />}
</>
)}
{currentPage === COMMAND_ROUTES.AI && <AiCommand />}
{currentPage === COMMAND_ROUTES.DOCS_SEARCH && <DocsSearch />}
{currentPage === COMMAND_ROUTES.GENERATE_SQL && <GenerateSQL />}
{currentPage === COMMAND_ROUTES.THEME && <ThemeOptions />}
{currentPage === COMMAND_ROUTES.API_KEYS && <APIKeys />}
</CommandList>
</CommandDialog>
</>
)
}
export default CommandMenu