'use client' import { User } from 'lucide-react' import { Fragment, useCallback, useEffect, useRef, useState } from 'react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import { useBreakpoint } from 'common' import { AiIconAnimation, Button, CommandGroup_Shadcn_, CommandItem_Shadcn_, CommandList_Shadcn_, cn, markdownComponents, } from 'ui' import { StatusIcon } from 'ui/src/components/StatusIcon' import { Breadcrumb, CommandHeader, CommandInput, CommandWrapper, generateCommandClassNames, useHistoryKeys, useQuery, useSetCommandMenuSize, useSetQuery, } from '../..' import { AiWarning, Message, MessageRole, MessageStatus, useAiChat } from '../ai' 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?', ] const DocsAiPage = () => { const setQuery = useSetQuery() const isBelowSm = useBreakpoint('sm') /** * Interface for AI interaction is larger to allow more reading space. */ useSetCommandMenuSize('xlarge') const [isLoading, setIsLoading] = useState(false) const { submit, reset, messages, isResponding, hasError } = useAiChat({ setIsLoading, }) const handleSubmit = useCallback( (message: string) => { setQuery('') submit(message) }, [submit] ) const handleReset = useCallback(() => { setQuery('') reset() }, [reset]) return ( {isBelowSm && ( )}
{!hasError && messages.length > 0 && } {!hasError && messages.length === 0 && } {hasError && }
{!isBelowSm && ( )} {messages.length > 0 && !hasError && ( )}
) } function PromptInput({ submit, reset, messages, isLoading, isResponding, className, }: { submit: (query: string) => void reset: () => void messages: Array isLoading: boolean isResponding: boolean className?: string }) { const query = useQuery() const previousQuery = useRef(query) const setQuery = useSetQuery() // If the user has already typed something when they select Supabase AI, we want to // submit it immediately. useEffect(() => { if (query) { submit(query) } return reset }, []) const [inputValue, setInputValue] = useState('') // Support CJK IME input const [isImeComposing, setIsImeComposing] = useState(false) useEffect(() => { if (query !== previousQuery.current) { setInputValue(query) previousQuery.current = query } else if (!isImeComposing) { setQuery(inputValue) previousQuery.current = inputValue } }, [inputValue, query, isImeComposing]) useHistoryKeys({ enable: !isResponding, stack: messages.filter(({ role }) => role === MessageRole.User).map(({ content }) => content), }) return ( setIsImeComposing(true)} onCompositionEnd={() => setIsImeComposing(false)} onKeyDown={(e) => { switch (e.key) { case 'Enter': if (!query || isLoading || isResponding || isImeComposing) { return } return submit(query) default: return } }} /> ) } function AiMessages({ messages }: { messages: Array }) { return ( <> {/* cmdk menus need a CommandList descendent in order to not throw an error. This display doesn't actually need the Command Menu, but it's needed for the empty state to work with the input, so this is a somewhat hacktastic way of making it not error. */}
{messages.map((message, index) => { switch (message.role) { case MessageRole.User: return (
You
{message.content}
) case MessageRole.Assistant: return (
Supabase AI
{message.status === MessageStatus.Pending && ( )} , }} className="prose dark:prose-dark break-words" > {message.content}
) } })}
) } function EmptyState({ handleSubmit }: { handleSubmit: (message: string) => void }) { const query = useQuery() return ( {questions.map((question) => { const key = question.replace(/\s+/g, '_') return ( { if (!query) { handleSubmit(question) } }} key={key} > {question} ) })} ) } function ErrorState({ handleReset }: { handleReset: () => void }) { return (

Sorry, looks like Supabase AI is having a hard time!

Please try again in a bit.

) } export { DocsAiPage }