import { SSE } from 'sse.js' import { format } from 'sql-formatter' import type { CreateCompletionResponse } from 'openai' import { useCallback, useEffect, useReducer, useRef, useState } from 'react' import { Button, CodeBlock, IconAlertCircle, IconAlertTriangle, IconCheck, IconClipboard, IconCornerDownLeft, IconSave, IconUser, Input, } from 'ui' import { cn } from './../../utils/cn' import { AiIcon, AiIconChat } from './Command.icons' import { CommandItem } from './Command.utils' import { useCommandMenu } from './CommandMenuProvider' import CopyToClipboard from 'react-copy-to-clipboard' import { SAMPLE_QUERIES } from './Command.constants' function getEdgeFunctionUrl() { const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL?.replace(/\/$/, '') if (!supabaseUrl) return undefined // https://github.com/supabase/supabase-js/blob/10d3423506cbd56345f7f6ab2ec2093c8db629d4/src/SupabaseClient.ts#L96 const isPlatform = supabaseUrl.match(/(supabase\.co)|(supabase\.in)/) if (isPlatform) { const [schemeAndProjectId, domain, tld] = supabaseUrl.split('.') return `${schemeAndProjectId}.functions.${domain}.${tld}` } else { return `${supabaseUrl}/functions/v1` } } function promptDataReducer( state: any[], action: { index?: number answer?: string | undefined status?: string query?: string | undefined type?: 'remove-last-item' | string } ) { // set a standard state to use later let current = [...state] if (action.type) { switch (action.type) { case 'remove-last-item': current.pop() return [...current] default: break } } // check that an index is present if (action.index === undefined) return [...state] if (!current[action.index]) { current[action.index] = { query: '', answer: '', status: '' } } current[action.index].answer = action.answer if (action.query) { current[action.index].query = action.query } if (action.status) { current[action.index].status = action.status } return [...current] } const SQLOutputActions = ({ answer }: { answer: string }) => { const [showCopied, setShowCopied] = useState(false) const [isSaving, setIsSaving] = useState(false) const [isSaved, setIsSaved] = useState(false) const { project, onSaveGeneratedSQL } = useCommandMenu() const applyCallback = () => onSaveGeneratedSQL !== undefined ? new Promise((resolve) => onSaveGeneratedSQL(answer, resolve)) : {} const onSelectSaveSnippet = async () => { setIsSaving(true) await applyCallback() setIsSaved(true) setIsSaving(false) } useEffect(() => { if (!showCopied) return const timer = setTimeout(() => setShowCopied(false), 2000) return () => clearTimeout(timer) }, [showCopied]) useEffect(() => { if (!isSaved) return const timer = setTimeout(() => setIsSaved(false), 2000) return () => clearTimeout(timer) }, [isSaved]) return (
{project?.ref !== undefined && onSaveGeneratedSQL !== undefined && ( )}
) } const GenerateSQL = () => { const [promptIndex, setPromptIndex] = useState(0) const [answer, setAnswer] = useState('') const [isResponding, setIsResponding] = useState(false) const [hasClippyError, setHasClippyError] = useState(false) const [selectedCategory, setSelectedCategory] = useState(SAMPLE_QUERIES[0].category) const eventSourceRef = useRef() const [promptData, dispatchPromptData] = useReducer(promptDataReducer, []) const { isLoading, setIsLoading, search, setSearch } = useCommandMenu() const cantHelp = answer?.trim() === "Sorry, I don't know how to help with that." const handleConfirm = useCallback( async (query: string) => { const edgeFunctionUrl = getEdgeFunctionUrl() if (!edgeFunctionUrl) { return console.error('No edge function url') } setAnswer(undefined) setSearch('') dispatchPromptData({ index: promptIndex, answer: undefined, query }) setIsResponding(false) setHasClippyError(false) setIsLoading(true) // [Joshen] Eventually we need to pass the table data in here as well const queryToSend = ` Generate a Postgres SQL query based on the following natural language prompt. For primary keys, always use "integer primary key generated always as identity": ${query} Postgres SQL query: `.trim() 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: queryToSend, context: promptData }), }) function handleError(err: T) { setIsLoading(false) setIsResponding(false) setHasClippyError(true) console.error(err) } eventSource.addEventListener('error', handleError) eventSource.addEventListener('message', (e: any) => { try { setIsLoading(false) if (e.data === '[DONE]') { setIsResponding(false) setAnswer(undefined) setPromptIndex((x) => { return x + 1 }) return } setIsResponding(true) const completionResponse: CreateCompletionResponse = JSON.parse(e.data) const [{ text: content }] = completionResponse.choices const text = content ?? '' setAnswer((answer) => { const currentAnswer = answer ?? '' dispatchPromptData({ index: promptIndex, answer: currentAnswer + text }) return (answer ?? '') + text }) } catch (err) { handleError(err) } }) eventSource.stream() eventSourceRef.current = eventSource setIsLoading(true) }, [promptIndex, promptData] ) function handleResetPrompt() { eventSourceRef.current?.close() eventSourceRef.current = undefined setSearch('') setAnswer(undefined) setIsResponding(false) setHasClippyError(false) } useEffect(() => { if (search) { handleConfirm(search) } }, []) const formatAnswer = (answer: string) => { try { return format(answer, { language: 'postgresql', keywordCase: 'lower', }) } catch (error: any) { return answer } } return (
e.stopPropagation()}>
{promptData.map((prompt, i) => { if (!prompt.query) return <> const formattedPromptAnwswer = (prompt?.answer ?? '') .replace(/`/g, '') .replace(/sql\n/g, '') .trim() const promptAnswer = isResponding ? formattedPromptAnwswer : formatAnswer(formattedPromptAnwswer) return ( <> {prompt.query && (
{prompt.query}
)}
{cantHelp ? (

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

) : (
<> {isLoading && promptIndex === i ? (
) : (
{promptAnswer} {!isResponding && }
)}
)}
) })} {promptData.length === 0 && !hasClippyError && (

Example queries

Use these example queries to help get your project started quickly.

    {SAMPLE_QUERIES.map((item, index) => (
  • setSelectedCategory(item.category)} className={cn( 'px-4 py-1 cursor-pointer text-sm hover:bg-slate-300 rounded-md', selectedCategory === item.category && 'bg-slate-400 ' )} > {item.category}
  • ))}
    {SAMPLE_QUERIES.find((item) => item.category === selectedCategory)?.queries.map( (query, index) => ( { if (!search) { handleConfirm(query) } }} forceMount key={query.replace(/\s+/g, '_')} >

    {query}

    ) )}
)} {hasClippyError && (

Sorry, looks like Clippy is having a hard time!

Please try again in a bit.

)}
{!isLoading && !isResponding ? (
Submit message
) : null} } onChange={(e) => { if (!isLoading || !isResponding) { setSearch(e.target.value) } }} onKeyDown={(e) => { switch (e.key) { case 'Enter': if (!search) { return } if (isLoading || isResponding) { return } handleConfirm(search) return default: return } }} />
) } export default GenerateSQL