From d6b7c1b7ba8e9f85822fd88efcac637765e17d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Tue, 5 Dec 2023 17:50:26 +0100 Subject: [PATCH] Revert "feat: Add streaming responses to the new RLS Editor (#19304)" (#19439) This reverts commit a2b01e0bc5d71a376ff144257d44204141d68c01. --- apps/database-new/app/NewThreadInput.tsx | 3 +- .../app/[threadId]/[runId]/ChatInput.tsx | 3 +- apps/studio/app/api/ai/sql/assistant/route.ts | 113 ------ apps/studio/app/dummy/page.tsx | 9 - apps/studio/app/layout.tsx | 3 - .../AIPolicyEditorPanel/AIPolicyChat.tsx | 43 +- .../AIPolicyEditorPanel.utils.ts | 30 +- .../AIPolicyEditorPanel/AIPolicyPre.tsx | 82 ---- .../Policies/AIPolicyEditorPanel/Message.tsx | 100 ++++- .../Policies/AIPolicyEditorPanel/index.tsx | 139 ++++--- .../components/ui/CodeEditor/CodeEditor.tsx | 9 +- apps/studio/data/ai/keys.ts | 3 + apps/studio/data/ai/rls-suggest-mutation.ts | 67 ++++ apps/studio/data/ai/rls-suggest-query.ts | 37 ++ apps/studio/next-env.d.ts | 1 - apps/studio/package.json | 1 - .../sql/suggest/[thread_id]/[run_id]/index.ts | 54 +++ apps/studio/pages/api/ai/sql/suggest/index.ts | 85 ++++ apps/studio/tsconfig.json | 9 +- package-lock.json | 379 +----------------- packages/ui/index.tsx | 1 + .../ui/src/patterns}/AssistantChatForm.tsx | 5 +- 22 files changed, 490 insertions(+), 686 deletions(-) delete mode 100644 apps/studio/app/api/ai/sql/assistant/route.ts delete mode 100644 apps/studio/app/dummy/page.tsx delete mode 100644 apps/studio/app/layout.tsx delete mode 100644 apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyPre.tsx create mode 100644 apps/studio/data/ai/keys.ts create mode 100644 apps/studio/data/ai/rls-suggest-mutation.ts create mode 100644 apps/studio/data/ai/rls-suggest-query.ts create mode 100644 apps/studio/pages/api/ai/sql/suggest/[thread_id]/[run_id]/index.ts create mode 100644 apps/studio/pages/api/ai/sql/suggest/index.ts rename {apps/database-new/components => packages/ui/src/patterns}/AssistantChatForm.tsx (97%) diff --git a/apps/database-new/app/NewThreadInput.tsx b/apps/database-new/app/NewThreadInput.tsx index 0f9f118a57e..7549eef52b0 100644 --- a/apps/database-new/app/NewThreadInput.tsx +++ b/apps/database-new/app/NewThreadInput.tsx @@ -6,10 +6,9 @@ import { createClient } from '@/lib/supabase/client' import { ExternalLink } from 'lucide-react' import { useEffect, useState } from 'react' import { useFormState } from 'react-dom' +import { AssistantChatForm, cn } from 'ui' import { createThread } from './actions' import { useRouter } from 'next/navigation' -import { AssistantChatForm } from '@/components/AssistantChatForm' -import { cn } from 'ui' const suggestions = CHAT_EXAMPLES diff --git a/apps/database-new/app/[threadId]/[runId]/ChatInput.tsx b/apps/database-new/app/[threadId]/[runId]/ChatInput.tsx index 08ba566f735..2678473deae 100644 --- a/apps/database-new/app/[threadId]/[runId]/ChatInput.tsx +++ b/apps/database-new/app/[threadId]/[runId]/ChatInput.tsx @@ -1,10 +1,9 @@ 'use client' import { updateThread } from '@/app/actions' -import { AssistantChatForm } from '@/components/AssistantChatForm' import { useState } from 'react' import { useFormState, useFormStatus } from 'react-dom' -import { Input } from 'ui' +import { AssistantChatForm, Input } from 'ui' function ChatInput({ params }: { params: { threadId: string; runId: string } }) { const [value, setValue] = useState('') diff --git a/apps/studio/app/api/ai/sql/assistant/route.ts b/apps/studio/app/api/ai/sql/assistant/route.ts deleted file mode 100644 index 012348a1131..00000000000 --- a/apps/studio/app/api/ai/sql/assistant/route.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { OpenAIStream, StreamingTextResponse } from 'ai' -import { codeBlock, oneLine, stripIndent } from 'common-tags' -import { NextRequest } from 'next/server' -import OpenAI from 'openai' - -export const runtime = 'edge' - -const openAiKey = process.env.OPENAI_KEY - -/** - * This route has to be with an edge runtime to support streaming. This route is not using the - * apiWrapper because it's not working with edge API routes (different params) but the route doesn't - * require authentication. - */ -export async function POST(request: NextRequest) { - if (!openAiKey) { - return new Response( - JSON.stringify({ - error: 'No OPENAI_KEY set. Create this environment variable to use AI features.', - }), - { - status: 500, - headers: { 'Content-Type': 'application/json' }, - } - ) - } - - const openai = new OpenAI({ apiKey: openAiKey }) - - const body = await (request.json() as Promise<{ - messages: { content: string; role: 'user' | 'assistant' }[] - entityDefinitions: string[] - policyDefinition: string - }>) - - const { messages, entityDefinitions, policyDefinition } = body - - const initMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [ - { - role: 'system', - content: stripIndent` - You're an Postgres expert in writing row level security policies. Your purpose is to - generate a policy with the constraints given by the user. You will be provided a schema - on which the policy should be applied. - - The output should use the following instructions: - - The generated SQL must be valid SQL. - - Always use double apostrophe in SQL strings (eg. 'Night''s watch') - - You can use only CREATE POLICY queries, no other queries are allowed. - - You can add short explanations to your messages. - - The result should be a valid markdown. The SQL code should be wrapped in \`\`\`. - - Always use "auth.uid()" instead of "current_user". - - Only use "WITH CHECK" on INSERT or UPDATE policies. - - The policy name should be short text explaining the policy, enclosed in double quotes. - - Always make sure that every \`\`\` has a corresponding ending tag \`\`\`. - - Always put explanations as separate text. Don't use inline SQL comments. - - The output should look like this: - "CREATE POLICY user_policy ON users FOR INSERT USING (user_name = current_user) WITH (true);" - `, - }, - ] - - if (entityDefinitions) { - const definitions = codeBlock`${entityDefinitions.join('\n\n')}` - initMessages.push({ - role: 'user', - content: oneLine`Here is my database schema for reference: ${definitions}`, - }) - } - - if (policyDefinition !== undefined) { - initMessages.push({ - role: 'user', - content: codeBlock` - Here is my policy definition for reference: - ${policyDefinition} - `.trim(), - }) - } - - if (messages) { - initMessages.push(...messages) - } - - const completionOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { - model: 'gpt-3.5-turbo-1106', - messages: initMessages, - max_tokens: 1024, - temperature: 0, - stream: true, - } - - try { - const response = await openai.chat.completions.create(completionOptions) - // Proxy the streamed SSE response from OpenAI - const stream = OpenAIStream(response) - - return new StreamingTextResponse(stream) - } catch (error: any) { - console.error(error) - - return new Response( - JSON.stringify({ - error: 'There was an error processing your request', - }), - { - status: 500, - headers: { 'Content-Type': 'application/json' }, - } - ) - } -} diff --git a/apps/studio/app/dummy/page.tsx b/apps/studio/app/dummy/page.tsx deleted file mode 100644 index 2a6db51950e..00000000000 --- a/apps/studio/app/dummy/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -/** - * This dummy page is added because of a bug in Nextjs. If you only have API routes in /app folder, - * for some reason it adds the base path twice to some of the links in the /pages folder - * (database/indexes for example). The only way is to have a dummy page which somehow calms Nextjs - * down. This solution was found in https://github.com/vercel/next.js/issues/54770. - */ -export default function Page() { - return <> -} diff --git a/apps/studio/app/layout.tsx b/apps/studio/app/layout.tsx deleted file mode 100644 index b95283c64c8..00000000000 --- a/apps/studio/app/layout.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function RootLayout({ children }: { children: React.ReactNode }) { - return <>{children} -} diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyChat.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyChat.tsx index 3177a07eef2..ce46b475d7c 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyChat.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyChat.tsx @@ -1,7 +1,8 @@ import { zodResolver } from '@hookform/resolvers/zod' -import { compact, last } from 'lodash' +import { compact, last, sortBy } from 'lodash' import { Loader2 } from 'lucide-react' -import { useEffect, useRef } from 'react' +import OpenAI from 'openai' +import { useEffect, useMemo, useRef } from 'react' import { useForm } from 'react-hook-form' import { AiIcon, @@ -17,11 +18,10 @@ import * as z from 'zod' import { useProfile } from 'lib/profile' import { useAppStateSnapshot } from 'state/app-state' -import { MessageWithDebug } from './AIPolicyEditorPanel.utils' import Message from './Message' interface AIPolicyChatProps { - messages: MessageWithDebug[] + messages: OpenAI.Beta.Threads.Messages.ThreadMessage[] loading: boolean onSubmit: (s: string) => void onDiff: (s: string) => void @@ -40,6 +40,14 @@ export const AIPolicyChat = ({ const bottomRef = useRef(null) const name = compact([profile?.first_name, profile?.last_name]).join(' ') + const sorted = useMemo(() => { + return sortBy(messages, (m) => m.created_at).filter((m) => { + if (m.content[0].type === 'text') { + return !m.content[0].text.value.startsWith('Here is my database schema for reference:') + } + return false + }) + }, [messages]) const FormSchema = z.object({ chat: z.string() }) const form = useForm>({ @@ -49,16 +57,16 @@ export const AIPolicyChat = ({ defaultValues: { chat: '' }, }) const formChatValue = form.getValues().chat - const pendingReply = loading && last(messages)?.role === 'user' + const pendingReply = loading && last(sorted)?.role === 'user' - // try to scroll on each rerender to the bottom useEffect(() => { - if (loading && bottomRef.current) { + // 👇️ scroll to bottom every time messages change + if (bottomRef.current) { setTimeout(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, 500) } - }) + }, [messages.length]) useEffect(() => { if (!loading) { @@ -72,10 +80,9 @@ export const AIPolicyChat = ({ }, [formChatValue]) return ( -
+
- {messages.map((m) => ( + {sorted.map((m, idx) => ( ))} - {pendingReply && } + {pendingReply && }
diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyEditorPanel.utils.ts b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyEditorPanel.utils.ts index 4ab756976c9..108072df1ca 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyEditorPanel.utils.ts +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyEditorPanel.utils.ts @@ -1,24 +1,36 @@ import { PostgresPolicy } from '@supabase/postgres-meta' -import { Message } from 'ai/react' import { uuidv4 } from 'lib/helpers' - -export type MessageWithDebug = Message & { isDebug: boolean } +import { ThreadMessage } from 'openai/resources/beta/threads/messages/messages' export const generateThreadMessage = ({ id, + threadId, + runId, content, - isDebug, + metadata = {}, }: { id?: string + threadId?: string + runId?: string content: string - isDebug: boolean + metadata?: any }) => { - const message: MessageWithDebug = { + const message: ThreadMessage = { id: id ?? uuidv4(), + object: 'thread.message', role: 'assistant', - content, - createdAt: new Date(), - isDebug: isDebug, + file_ids: [], + metadata, + content: [ + { + type: 'text', + text: { value: content, annotations: [] }, + }, + ], + created_at: Math.floor(Number(new Date()) / 1000), + assistant_id: null, + thread_id: threadId ?? '', + run_id: runId ?? '', } return message } diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyPre.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyPre.tsx deleted file mode 100644 index 0c7dd0e5cab..00000000000 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyPre.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import * as Tooltip from '@radix-ui/react-tooltip' -import { Copy, FileDiff } from 'lucide-react' -import { format } from 'sql-formatter' -import { Button, CodeBlock, cn } from 'ui' - -interface AAIPolicyPreProps { - onDiff: (s: string) => void - children: string[] -} - -export const AIPolicyPre = ({ onDiff, children }: AAIPolicyPreProps) => { - let formatted = (children || [''])[0] - try { - formatted = format(formatted, { language: 'postgresql' }) - } catch {} - - if (formatted.length === 0) { - return null - } - return ( -
-      code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap'
-        )}
-        hideCopy
-        hideLineNumbers
-      />
-      
- - - - - - - -
- Apply changes -
-
-
-
- - - - - - - -
- Copy code -
-
-
-
-
-
- ) -} diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/Message.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/Message.tsx index 1d289648ee3..8bbf422b991 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/Message.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/Message.tsx @@ -1,17 +1,19 @@ +import * as Tooltip from '@radix-ui/react-tooltip' import dayjs from 'dayjs' -import { noop } from 'lodash' +import { kebabCase, noop, take } from 'lodash' +import { Copy, FileDiff } from 'lucide-react' import Image from 'next/image' import { PropsWithChildren, memo, useMemo } from 'react' import ReactMarkdown from 'react-markdown' -import remarkGfm from 'remark-gfm' -import { AiIconAnimation, Badge, markdownComponents } from 'ui' +import { format } from 'sql-formatter' +import { AiIconAnimation, Badge, Button, IconSettings } from 'ui' +import CodeEditor from 'components/ui/CodeEditor' import { useProfile } from 'lib/profile' -import { AIPolicyPre } from './AIPolicyPre' interface MessageProps { name?: string - role: 'function' | 'user' | 'assistant' | 'system' + role: 'user' | 'assistant' content?: string createdAt?: number isDebug?: boolean @@ -46,27 +48,101 @@ const Message = memo(function Message({ />
) - }, [content, profile?.username, role]) + }, [role]) if (!content) return null return ( -
+
{icon} {role === 'assistant' ? 'Assistant' : name ? name : 'You'} {createdAt && ( - {dayjs(createdAt).fromNow()} + {dayjs(createdAt * 1000).fromNow()} )} {isDebug && Debug request}
{ - return {props.children[0].props.children} + p: ({ children }) =>
{children}
, + // intentionally rendering as pre. The other approach would be to render as code element, + // but that will render elements which appear in the explanations as Monaco editors. + pre: ({ children }) => { + const code = (children[0] as any).props.children[0] as string + let formatted = code + try { + formatted = format(code) + } catch {} + + // create a key from the name of the generated policy so that we're sure it's unique + const key = kebabCase(take(code.split(' '), 3).join(' ')) + + return ( +
+ +
+ + + + + + + +
+ Apply changes +
+
+
+
+ + + + + + + +
+ Copy code +
+
+
+
+
+
+ ) }, }} > diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx index f4f032c23b5..f5eb371da3c 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx @@ -1,7 +1,7 @@ import { PostgresPolicy } from '@supabase/postgres-meta' -import { useChat } from 'ai/react' import { FileDiff } from 'lucide-react' import dynamic from 'next/dynamic' +import { ThreadMessage } from 'openai/resources/beta/threads/messages/messages' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import toast from 'react-hot-toast' import { Button, Modal, SheetContent_Shadcn_, SheetFooter_Shadcn_, Sheet_Shadcn_, cn } from 'ui' @@ -11,16 +11,16 @@ import { IStandaloneDiffEditor, } from 'components/interfaces/SQLEditor/SQLEditor.types' import ConfirmationModal from 'components/ui/ConfirmationModal' +import { useRlsSuggestMutation } from 'data/ai/rls-suggest-mutation' +import { useRlsSuggestQuery } from 'data/ai/rls-suggest-query' import { useSqlDebugMutation } from 'data/ai/sql-debug-mutation' import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query' import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' import { useSelectedOrganization, useSelectedProject, useStore } from 'hooks' -import { BASE_PATH, OPT_IN_TAGS } from 'lib/constants' +import { OPT_IN_TAGS } from 'lib/constants' import { uuidv4 } from 'lib/helpers' -import { sortBy, uniqBy } from 'lodash' import { AIPolicyChat } from './AIPolicyChat' import { - MessageWithDebug, generatePlaceholder, generatePolicyDefinition, generateThreadMessage, @@ -53,8 +53,6 @@ export const AIPolicyEditorPanel = memo(function ({ const selectedProject = useSelectedProject() const selectedOrganization = useSelectedOrganization() - // use chat id because useChat doesn't have a reset function to clear all messages - const [chatId, setChatId] = useState(uuidv4()) const editorRef = useRef(null) const diffEditorRef = useRef(null) const placeholder = generatePlaceholder(selectedPolicy) @@ -63,13 +61,32 @@ export const AIPolicyEditorPanel = memo(function ({ const [error, setError] = useState() const [showDetails, setShowDetails] = useState(false) // [Joshen] Separate state here as there's a delay between submitting and the API updating the loading status - const [debugThread, setDebugThread] = useState([]) + const [loading, setLoading] = useState(false) + const [keepPreviousData, setKeepPreviousData] = useState(false) + const [debugThread, setDebugThread] = useState([]) const [assistantVisible, setAssistantPanel] = useState(false) + const [ids, setIds] = useState<{ threadId: string; runId: string } | undefined>(undefined) const [isAssistantChatInputEmpty, setIsAssistantChatInputEmpty] = useState(true) const [incomingChange, setIncomingChange] = useState(undefined) // used for confirmation when closing the panel with unsaved changes const [isClosingPolicyEditorPanel, setIsClosingPolicyEditorPanel] = useState(false) + const { data } = useRlsSuggestQuery( + { thread_id: ids?.threadId!, run_id: ids?.runId! }, + { + enabled: !!(ids?.runId && ids.threadId), + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchInterval: (data) => { + if (data && data.status === 'completed') { + return Infinity + } + return 5000 + }, + keepPreviousData, + } + ) + const { data: entities } = useEntityDefinitionsQuery( { projectRef: selectedProject?.ref, @@ -80,30 +97,23 @@ export const AIPolicyEditorPanel = memo(function ({ const entityDefinitions = entities?.map((def) => def.sql.trim()) - const { - messages: chatMessages, - append, - isLoading, - } = useChat({ - id: chatId, - api: `${BASE_PATH}/api/ai/sql/assistant`, - body: { - entityDefinitions: isOptedInToAI ? entityDefinitions : undefined, - policyDefinition: - selectedPolicy !== undefined ? generatePolicyDefinition(selectedPolicy) : undefined, + const { mutate: addPromptMutation } = useRlsSuggestMutation({ + onSuccess: (data) => { + setIds({ threadId: data.threadId, runId: data.runId }) + }, + onError: (error) => { + const threadMessage = generateThreadMessage({ + threadId: ids?.threadId, + runId: ids?.runId, + content: error.message.includes('No OPENAI_KEY set') + ? `Seems like you haven't set an OPENAI_KEY in your environment variables of the dashboard. Update your .env file with that environment variable to use all the AI features of the dashboard!` + : error.message, + }) + setDebugThread([...debugThread, threadMessage]) + setLoading(false) }, }) - const messages = useMemo(() => { - const merged = [...debugThread, ...chatMessages.map((m) => ({ ...m, isDebug: false }))] - - return merged.sort( - (a, b) => - (a.createdAt?.getTime() ?? 0) - (b.createdAt?.getTime() ?? 0) || - a.role.localeCompare(b.role) - ) - }, [chatMessages, debugThread]) - const { mutate: executeMutation, isLoading: isExecuting } = useExecuteSqlMutation({ onSuccess: () => { // refresh all policies @@ -118,6 +128,37 @@ export const AIPolicyEditorPanel = memo(function ({ const { mutateAsync: debugSql, isLoading: isDebugSqlLoading } = useSqlDebugMutation() + const addPrompt = useCallback( + (message: string) => { + setLoading(true) + if (ids?.threadId) { + addPromptMutation({ + thread_id: ids?.threadId, + prompt: message, + }) + } else { + addPromptMutation({ + thread_id: ids?.threadId, + prompt: message, + entityDefinitions: isOptedInToAI ? entityDefinitions : undefined, + policyDefinition: + selectedPolicy !== undefined ? generatePolicyDefinition(selectedPolicy) : undefined, + }) + } + }, + [addPromptMutation, entityDefinitions, ids?.threadId] + ) + + const messages = useMemo( + () => [ + ...(data?.messages ?? []).sort( + (a, b) => b.created_at - a.created_at || a.role.localeCompare(b.role) + ), + ...debugThread, + ], + [data?.messages, debugThread] + ) + const errorLines = error?.formattedError.split('\n').filter((x: string) => x.length > 0).length ?? 0 @@ -183,8 +224,10 @@ export const AIPolicyEditorPanel = memo(function ({ const assistantMessageBefore = generateThreadMessage({ id: messageId, + threadId: ids?.threadId, + runId: ids?.runId, content: 'Thinking...', - isDebug: true, + metadata: { type: 'debug' }, }) setDebugThread([...debugThread, assistantMessageBefore]) @@ -196,28 +239,38 @@ export const AIPolicyEditorPanel = memo(function ({ const assistantMessageAfter = generateThreadMessage({ id: messageId, + threadId: ids?.threadId, + runId: ids?.runId, content: `${solution}\n\`\`\`sql\n${sql}\n\`\`\``, - isDebug: true, + metadata: { type: 'debug' }, }) - const cleanedMessages = uniqBy([...debugThread, assistantMessageAfter], (m) => m.id) - - setDebugThread(cleanedMessages) + setDebugThread([...debugThread, assistantMessageAfter]) } + const onDiff = useCallback((v: string) => setIncomingChange(v), []) + // when the panel is closed, reset all values useEffect(() => { if (!visible) { - editorRef.current?.setValue('') - setIncomingChange(undefined) - setAssistantPanel(false) + const policy = editorRef.current?.getValue() + if (policy) editorRef.current?.setValue('') + if (incomingChange) setIncomingChange(undefined) + if (assistantVisible) setAssistantPanel(false) setIsClosingPolicyEditorPanel(false) + setIds(undefined) setError(undefined) setDebugThread([]) - setChatId(uuidv4()) + setKeepPreviousData(false) setShowDetails(false) + } else { + setKeepPreviousData(true) } }, [visible]) + useEffect(() => { + if (data?.status === 'completed') setLoading(false) + }, [data?.status]) + // [Joshen] Problem with monaco is that it's height cannot be dynamically updated once its initialized // So this is sort of a hacky way to do so, until we find a better solution at least const footerHeight = 58 @@ -336,16 +389,10 @@ export const AIPolicyEditorPanel = memo(function ({
- append({ - content: message, - role: 'user', - createdAt: new Date(), - }) - } - onDiff={setIncomingChange} + onSubmit={(message: string) => addPrompt(message)} + onDiff={onDiff} onChange={setIsAssistantChatInputEmpty} - loading={isLoading || isDebugSqlLoading} + loading={loading || data?.status === 'loading'} />
)} diff --git a/apps/studio/components/ui/CodeEditor/CodeEditor.tsx b/apps/studio/components/ui/CodeEditor/CodeEditor.tsx index abfa83028bc..cc41d4b060f 100644 --- a/apps/studio/components/ui/CodeEditor/CodeEditor.tsx +++ b/apps/studio/components/ui/CodeEditor/CodeEditor.tsx @@ -1,16 +1,15 @@ import Editor, { EditorProps, OnMount } from '@monaco-editor/react' import { merge, noop } from 'lodash' -import { editor } from 'monaco-editor' import { useRef } from 'react' -import { cn } from 'ui' import { timeout } from 'lib/helpers' import Connecting from '../Loading' import { alignEditor } from './CodeEditor.utils' +import { cn } from 'ui' interface CodeEditorProps { id: string - language: 'pgsql' | 'json' | 'html' | undefined + language: 'pgsql' | 'json' | 'html' autofocus?: boolean defaultValue?: string isReadOnly?: boolean @@ -37,7 +36,7 @@ const CodeEditor = ({ options, value, }: CodeEditorProps) => { - const editorRef = useRef() + const editorRef = useRef() const onMount: OnMount = async (editor, monaco) => { editorRef.current = editor @@ -86,7 +85,7 @@ const CodeEditor = ({ theme="supabase" className={cn(className, 'monaco-editor')} value={value ?? undefined} - language={language} + defaultLanguage={language} defaultValue={defaultValue ?? undefined} loading={loading || } options={optionsMerged} diff --git a/apps/studio/data/ai/keys.ts b/apps/studio/data/ai/keys.ts new file mode 100644 index 00000000000..67eae9d36c9 --- /dev/null +++ b/apps/studio/data/ai/keys.ts @@ -0,0 +1,3 @@ +export const aiKeys = { + rlsSuggest: (threadId: string, runId: string) => [threadId, runId] as const, +} diff --git a/apps/studio/data/ai/rls-suggest-mutation.ts b/apps/studio/data/ai/rls-suggest-mutation.ts new file mode 100644 index 00000000000..0ab2d0bcf2e --- /dev/null +++ b/apps/studio/data/ai/rls-suggest-mutation.ts @@ -0,0 +1,67 @@ +import { useMutation, UseMutationOptions } from '@tanstack/react-query' +import toast from 'react-hot-toast' + +import { isResponseOk, post } from 'lib/common/fetch' +import { BASE_PATH } from 'lib/constants' +import { ResponseError } from 'types' +import { PostgresPolicy } from '@supabase/postgres-meta' + +export type RlsSuggestResponse = { + threadId: string + runId: string +} + +export type RlsSuggestVariables = { + thread_id?: string + prompt: string + entityDefinitions?: string[] + policyDefinition?: string +} + +export async function rlsSuggest({ + thread_id, + prompt, + entityDefinitions, + policyDefinition, +}: RlsSuggestVariables) { + const response = await post(BASE_PATH + '/api/ai/sql/suggest', { + thread_id, + prompt, + entityDefinitions, + policyDefinition, + }) + + if (!isResponseOk(response)) { + throw response.error + } + + return response +} + +type RlsSuggestData = Awaited> + +export const useRlsSuggestMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => rlsSuggest(vars), + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to prompt suggestion: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/apps/studio/data/ai/rls-suggest-query.ts b/apps/studio/data/ai/rls-suggest-query.ts new file mode 100644 index 00000000000..ed67cc78a15 --- /dev/null +++ b/apps/studio/data/ai/rls-suggest-query.ts @@ -0,0 +1,37 @@ +import { UseQueryOptions, useQuery } from '@tanstack/react-query' +import { get } from 'lib/common/fetch' +import { BASE_PATH } from 'lib/constants' +import OpenAI from 'openai' +import { aiKeys } from './keys' +import { ResponseError } from 'types' + +export type RlsSuggestVariables = { + thread_id: string + run_id: string +} + +export type RlsSuggestResponse = { + id: string + status: 'completed' | 'loading' + messages: OpenAI.Beta.Threads.Messages.ThreadMessage[] +} + +export async function rlsSuggest({ thread_id, run_id }: RlsSuggestVariables, signal?: AbortSignal) { + const response = await get(`${BASE_PATH}/api/ai/sql/suggest/${thread_id}/${run_id}`, { signal }) + if (response.error) throw response.error + + return response as RlsSuggestResponse +} + +export type RlsSuggestData = Awaited> +export type RlsSuggestDataError = ResponseError + +export const useRlsSuggestQuery = ( + { thread_id, run_id }: RlsSuggestVariables, + options: UseQueryOptions = {} +) => + useQuery( + aiKeys.rlsSuggest(thread_id, run_id), + ({ signal }) => rlsSuggest({ thread_id, run_id }, signal), + options + ) diff --git a/apps/studio/next-env.d.ts b/apps/studio/next-env.d.ts index fd36f9494e2..4f11a03dc6c 100644 --- a/apps/studio/next-env.d.ts +++ b/apps/studio/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/studio/package.json b/apps/studio/package.json index c77eb09e549..98b7ef7f6ee 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -38,7 +38,6 @@ "@tanstack/react-query-devtools": "^4.35.7", "@uidotdev/usehooks": "^2.4.1", "@zip.js/zip.js": "^2.7.29", - "ai": "^2.2.26", "ajv": "^8.6.3", "awesome-debounce-promise": "^2.1.0", "blueimp-md5": "^2.19.0", diff --git a/apps/studio/pages/api/ai/sql/suggest/[thread_id]/[run_id]/index.ts b/apps/studio/pages/api/ai/sql/suggest/[thread_id]/[run_id]/index.ts new file mode 100644 index 00000000000..8fdeefb83ee --- /dev/null +++ b/apps/studio/pages/api/ai/sql/suggest/[thread_id]/[run_id]/index.ts @@ -0,0 +1,54 @@ +import apiWrapper from 'lib/api/apiWrapper' +import { NextApiRequest, NextApiResponse } from 'next' +// This API function uses the new Threads API, which is only available on the v4 OpenAI lib. +import { OpenAI } from 'openai' + +const openAiKey = process.env.OPENAI_KEY + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!openAiKey) { + return res.status(500).json({ + error: 'No OPENAI_KEY set. Create this environment variable to use AI features.', + }) + } + + const { method } = req + + switch (method) { + case 'GET': + return handleGet(req, res) + default: + res.setHeader('Allow', ['GET']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +export async function handleGet(req: NextApiRequest, res: NextApiResponse) { + const openai = new OpenAI({ apiKey: openAiKey }) + + const { thread_id, run_id } = req.query as { thread_id: string; run_id: string } + + const [run, { data: messages }] = await Promise.all([ + openai.beta.threads.runs.retrieve(thread_id, run_id), + openai.beta.threads.messages.list(thread_id), + ]) + + let status = 'loading' + if (run.status === 'completed') { + status = 'completed' + } else if (run.status === 'failed') { + status = 'failed' + } + + const result = { + id: thread_id, + status, + messages: messages, + } + + return res.json(result) +} + +const wrapper = (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +export default wrapper diff --git a/apps/studio/pages/api/ai/sql/suggest/index.ts b/apps/studio/pages/api/ai/sql/suggest/index.ts new file mode 100644 index 00000000000..dc499377db9 --- /dev/null +++ b/apps/studio/pages/api/ai/sql/suggest/index.ts @@ -0,0 +1,85 @@ +import { codeBlock } from 'common-tags' +import apiWrapper from 'lib/api/apiWrapper' +import { NextApiRequest, NextApiResponse } from 'next' +// This API function uses the new Threads API, which is only available on the v4 OpenAI lib. +import { OpenAI } from 'openai' + +const openAiKey = process.env.OPENAI_KEY + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!openAiKey) { + return res.status(500).json({ + error: 'No OPENAI_KEY set. Create this environment variable to use AI features.', + }) + } + + const { method } = req + + switch (method) { + case 'POST': + return handlePost(req, res) + default: + res.setHeader('Allow', ['POST']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +export async function handlePost(req: NextApiRequest, res: NextApiResponse) { + const openai = new OpenAI({ apiKey: openAiKey }) + + try { + let { + body: { thread_id, prompt, entityDefinitions, policyDefinition }, + } = req + + if (!thread_id) { + const thread = await openai.beta.threads.create() + thread_id = thread.id + } + + const prerequisites = [] + + if (entityDefinitions) { + prerequisites.push( + codeBlock` + Here is my database schema for reference: + ${entityDefinitions.join('\n\n')} + `.trim() + ) + } + + if (policyDefinition !== undefined) { + prerequisites.push( + codeBlock` + Here is my policy definition for reference: + ${policyDefinition} + `.trim() + ) + } + + if (prerequisites.length > 0) { + await openai.beta.threads.messages.create(thread_id, { + role: 'user', + content: prerequisites.join('\n'), + }) + } + + await openai.beta.threads.messages.create(thread_id, { + role: 'user', + content: prompt, + }) + + const run = await openai.beta.threads.runs.create(thread_id, { + assistant_id: 'asst_O89dyQ2ttPVWfs8YEjKPFqAK', + }) + + return res.json({ threadId: thread_id, runId: run.id }) + } catch (e) { + console.log(e) + return res.status(500) + } +} + +const wrapper = (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +export default wrapper diff --git a/apps/studio/tsconfig.json b/apps/studio/tsconfig.json index ab5a90595ea..b03dcb6e92a 100644 --- a/apps/studio/tsconfig.json +++ b/apps/studio/tsconfig.json @@ -19,13 +19,8 @@ "paths": { "@ui/*": ["./../../packages/ui/src/*"] // handle ui package paths }, - "useDefineForClassFields": true, - "plugins": [ - { - "name": "next" - } - ] + "useDefineForClassFields": true }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", ".next/types/**/*.ts"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], "exclude": ["node_modules"] } diff --git a/package-lock.json b/package-lock.json index b3f382465df..e996ca6a363 100644 --- a/package-lock.json +++ b/package-lock.json @@ -725,7 +725,6 @@ "@tanstack/react-query-devtools": "^4.35.7", "@uidotdev/usehooks": "^2.4.1", "@zip.js/zip.js": "^2.7.29", - "ai": "^2.2.26", "ajv": "^8.6.3", "awesome-debounce-promise": "^2.1.0", "blueimp-md5": "^2.19.0", @@ -1225,6 +1224,7 @@ }, "node_modules/@ampproject/remapping": { "version": "2.2.1", + "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", @@ -2180,9 +2180,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", - "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", + "version": "7.23.0", + "dev": true, + "license": "MIT", "bin": { "parser": "bin/babel-parser.js" }, @@ -11722,160 +11722,6 @@ "version": "1.2.0", "license": "ISC" }, - "node_modules/@vue/compiler-core": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.9.tgz", - "integrity": "sha512-+/Lf68Vr/nFBA6ol4xOtJrW+BQWv3QWKfRwGSm70jtXwfhZNF4R/eRgyVJYoxFRhdCTk/F6g99BP0ffPgZihfQ==", - "peer": true, - "dependencies": { - "@babel/parser": "^7.23.3", - "@vue/shared": "3.3.9", - "estree-walker": "^2.0.2", - "source-map-js": "^1.0.2" - } - }, - "node_modules/@vue/compiler-core/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "peer": true - }, - "node_modules/@vue/compiler-dom": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.9.tgz", - "integrity": "sha512-nfWubTtLXuT4iBeDSZ5J3m218MjOy42Vp2pmKVuBKo2/BLcrFUX8nCSr/bKRFiJ32R8qbdnnnBgRn9AdU5v0Sg==", - "peer": true, - "dependencies": { - "@vue/compiler-core": "3.3.9", - "@vue/shared": "3.3.9" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.9.tgz", - "integrity": "sha512-wy0CNc8z4ihoDzjASCOCsQuzW0A/HP27+0MDSSICMjVIFzk/rFViezkR3dzH+miS2NDEz8ywMdbjO5ylhOLI2A==", - "peer": true, - "dependencies": { - "@babel/parser": "^7.23.3", - "@vue/compiler-core": "3.3.9", - "@vue/compiler-dom": "3.3.9", - "@vue/compiler-ssr": "3.3.9", - "@vue/reactivity-transform": "3.3.9", - "@vue/shared": "3.3.9", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.5", - "postcss": "^8.4.31", - "source-map-js": "^1.0.2" - } - }, - "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "peer": true - }, - "node_modules/@vue/compiler-sfc/node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", - "peer": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.9.tgz", - "integrity": "sha512-NO5oobAw78R0G4SODY5A502MGnDNiDjf6qvhn7zD7TJGc8XDeIEw4fg6JU705jZ/YhuokBKz0A5a/FL/XZU73g==", - "peer": true, - "dependencies": { - "@vue/compiler-dom": "3.3.9", - "@vue/shared": "3.3.9" - } - }, - "node_modules/@vue/reactivity": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.9.tgz", - "integrity": "sha512-VmpIqlNp+aYDg2X0xQhJqHx9YguOmz2UxuUJDckBdQCNkipJvfk9yA75woLWElCa0Jtyec3lAAt49GO0izsphw==", - "peer": true, - "dependencies": { - "@vue/shared": "3.3.9" - } - }, - "node_modules/@vue/reactivity-transform": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.9.tgz", - "integrity": "sha512-HnUFm7Ry6dFa4Lp63DAxTixUp8opMtQr6RxQCpDI1vlh12rkGIeYqMvJtK+IKyEfEOa2I9oCkD1mmsPdaGpdVg==", - "peer": true, - "dependencies": { - "@babel/parser": "^7.23.3", - "@vue/compiler-core": "3.3.9", - "@vue/shared": "3.3.9", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.5" - } - }, - "node_modules/@vue/reactivity-transform/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "peer": true - }, - "node_modules/@vue/reactivity-transform/node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", - "peer": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@vue/runtime-core": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.9.tgz", - "integrity": "sha512-xxaG9KvPm3GTRuM4ZyU8Tc+pMVzcu6eeoSRQJ9IE7NmCcClW6z4B3Ij6L4EDl80sxe/arTtQ6YmgiO4UZqRc+w==", - "peer": true, - "dependencies": { - "@vue/reactivity": "3.3.9", - "@vue/shared": "3.3.9" - } - }, - "node_modules/@vue/runtime-dom": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.9.tgz", - "integrity": "sha512-e7LIfcxYSWbV6BK1wQv9qJyxprC75EvSqF/kQKe6bdZEDNValzeRXEVgiX7AHI6hZ59HA4h7WT5CGvm69vzJTQ==", - "peer": true, - "dependencies": { - "@vue/runtime-core": "3.3.9", - "@vue/shared": "3.3.9", - "csstype": "^3.1.2" - } - }, - "node_modules/@vue/server-renderer": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.9.tgz", - "integrity": "sha512-w0zT/s5l3Oa3ZjtLW88eO4uV6AQFqU8X5GOgzq7SkQQu6vVr+8tfm+OI2kDBplS/W/XgCBuFXiPw6T5EdwXP0A==", - "peer": true, - "dependencies": { - "@vue/compiler-ssr": "3.3.9", - "@vue/shared": "3.3.9" - }, - "peerDependencies": { - "vue": "3.3.9" - } - }, - "node_modules/@vue/shared": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.9.tgz", - "integrity": "sha512-ZE0VTIR0LmYgeyhurPTpy4KzKsuDyQbMSdM49eKkMnT5X4VfFBLysMzjIZhLEFQYjjOVVfbvUDHckwjDFiO2eA==", - "peer": true - }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "license": "MIT", @@ -12196,43 +12042,6 @@ "node": ">=8" } }, - "node_modules/ai": { - "version": "2.2.26", - "resolved": "https://registry.npmjs.org/ai/-/ai-2.2.26.tgz", - "integrity": "sha512-ANvG9sTT/pwOykEj1Huos8BuiYD4azUVoMXxDV3uhWO1KdZP5iN1GnSnnh08+ZDXKhedZnXZqvVTG2rSEgCWPQ==", - "dependencies": { - "eventsource-parser": "1.0.0", - "nanoid": "3.3.6", - "solid-swr-store": "0.10.7", - "sswr": "2.0.0", - "swr": "2.2.0", - "swr-store": "0.10.6", - "swrv": "1.0.4" - }, - "engines": { - "node": ">=14.6" - }, - "peerDependencies": { - "react": "^18.2.0", - "solid-js": "^1.7.7", - "svelte": "^3.0.0 || ^4.0.0", - "vue": "^3.3.4" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "solid-js": { - "optional": true - }, - "svelte": { - "optional": true - }, - "vue": { - "optional": true - } - } - }, "node_modules/ajv": { "version": "6.12.6", "license": "MIT", @@ -14354,19 +14163,6 @@ "phenomenon": "^1.6.0" } }, - "node_modules/code-red": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", - "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", - "peer": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", - "@types/estree": "^1.0.1", - "acorn": "^8.10.0", - "estree-walker": "^3.0.3", - "periscopic": "^3.1.0" - } - }, "node_modules/codemirror": { "version": "5.65.15", "license": "MIT" @@ -17228,14 +17024,6 @@ "node": ">=0.8.x" } }, - "node_modules/eventsource-parser": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.0.0.tgz", - "integrity": "sha512-9jgfSCa3dmEme2ES3mPByGXfgZ87VbP97tng1G2nWwWx6bV2nYxm2AWCrbQjXToSe+yYlqaZNtxffR9IeQr95g==", - "engines": { - "node": ">=14.18" - } - }, "node_modules/execa": { "version": "5.1.1", "dev": true, @@ -22742,12 +22530,6 @@ "lie": "3.1.1" } }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "peer": true - }, "node_modules/locate-path": { "version": "6.0.0", "license": "MIT", @@ -30812,15 +30594,6 @@ "randombytes": "^2.1.0" } }, - "node_modules/seroval": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-0.14.1.tgz", - "integrity": "sha512-ZlC9y1KVDhZFdEHLYZup1RjKDutyX1tt3ffOauqRbRURa2vRr2NU/bHuVEuNEqR3zE2uCU3WM6LqH6Oinc3tWg==", - "peer": true, - "engines": { - "node": ">=10" - } - }, "node_modules/serve-static": { "version": "1.15.0", "dev": true, @@ -31079,28 +30852,6 @@ "node": ">= 10" } }, - "node_modules/solid-js": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.6.tgz", - "integrity": "sha512-yiH6ZfBBZ3xj/aU/PBpVKB+8r8WWp100NGF7k/Z0IrK9Y8Lv0jwvFiJY1cHdc6Tj7GqXArKnMBabM0m1k+LzkA==", - "peer": true, - "dependencies": { - "csstype": "^3.1.0", - "seroval": "^0.14.1" - } - }, - "node_modules/solid-swr-store": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/solid-swr-store/-/solid-swr-store-0.10.7.tgz", - "integrity": "sha512-A6d68aJmRP471aWqKKPE2tpgOiR5fH4qXQNfKIec+Vap+MGQm3tvXlT8n0I8UgJSlNAsSAUuw2VTviH2h3Vv5g==", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "solid-js": "^1.2", - "swr-store": "^0.10" - } - }, "node_modules/source-map": { "version": "0.6.1", "license": "BSD-3-Clause", @@ -31241,17 +30992,6 @@ "version": "4.0.0", "license": "ISC" }, - "node_modules/sswr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sswr/-/sswr-2.0.0.tgz", - "integrity": "sha512-mV0kkeBHcjcb0M5NqKtKVg/uTIYNlIIniyDfSGrSfxpEdM9C365jK0z55pl9K0xAkNTJi2OAOVFQpgMPUk+V0w==", - "dependencies": { - "swrev": "^4.0.0" - }, - "peerDependencies": { - "svelte": "^4.0.0" - } - }, "node_modules/stack-generator": { "version": "2.0.10", "license": "MIT", @@ -31942,61 +31682,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/svelte": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.7.tgz", - "integrity": "sha512-UExR1KS7raTdycsUrKLtStayu4hpdV3VZQgM0akX8XbXgLBlosdE/Sf3crOgyh9xIjqSYB3UEBuUlIQKRQX2hg==", - "peer": true, - "dependencies": { - "@ampproject/remapping": "^2.2.1", - "@jridgewell/sourcemap-codec": "^1.4.15", - "@jridgewell/trace-mapping": "^0.3.18", - "acorn": "^8.9.0", - "aria-query": "^5.3.0", - "axobject-query": "^3.2.1", - "code-red": "^1.0.3", - "css-tree": "^2.3.1", - "estree-walker": "^3.0.3", - "is-reference": "^3.0.1", - "locate-character": "^3.0.0", - "magic-string": "^0.30.4", - "periscopic": "^3.1.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/svelte/node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "peer": true, - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/svelte/node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", - "peer": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/svelte/node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "peer": true - }, "node_modules/swc-loader": { "version": "0.2.3", "dev": true, @@ -32028,41 +31713,6 @@ "node": ">= 4.7.0" } }, - "node_modules/swr": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz", - "integrity": "sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==", - "dependencies": { - "use-sync-external-store": "^1.2.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/swr-store": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/swr-store/-/swr-store-0.10.6.tgz", - "integrity": "sha512-xPjB1hARSiRaNNlUQvWSVrG5SirCjk2TmaUyzzvk69SZQan9hCJqw/5rG9iL7xElHU784GxRPISClq4488/XVw==", - "dependencies": { - "dequal": "^2.0.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/swrev": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/swrev/-/swrev-4.0.0.tgz", - "integrity": "sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA==" - }, - "node_modules/swrv": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/swrv/-/swrv-1.0.4.tgz", - "integrity": "sha512-zjEkcP8Ywmj+xOJW3lIT65ciY/4AL4e/Or7Gj0MzU3zBJNMdJiT8geVZhINavnlHRMMCcJLHhraLTAiDOTmQ9g==", - "peerDependencies": { - "vue": ">=3.2.26 < 4" - } - }, "node_modules/symbol-tree": { "version": "3.2.4", "dev": true, @@ -34154,27 +33804,6 @@ "version": "3.17.5", "license": "MIT" }, - "node_modules/vue": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.9.tgz", - "integrity": "sha512-sy5sLCTR8m6tvUk1/ijri3Yqzgpdsmxgj6n6yl7GXXCXqVbmW2RCXe9atE4cEI6Iv7L89v5f35fZRRr5dChP9w==", - "peer": true, - "dependencies": { - "@vue/compiler-dom": "3.3.9", - "@vue/compiler-sfc": "3.3.9", - "@vue/runtime-dom": "3.3.9", - "@vue/server-renderer": "3.3.9", - "@vue/shared": "3.3.9" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/w3c-keyname": { "version": "2.2.8", "license": "MIT", diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index e41302b8c91..4876d4deb62 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -182,6 +182,7 @@ export { Separator } from './src/components/shadcn/ui/separator' // patterns export * from './src/patterns/SchemaTableNode' +export { AssistantChatForm } from './src/patterns/AssistantChatForm' // links diff --git a/apps/database-new/components/AssistantChatForm.tsx b/packages/ui/src/patterns/AssistantChatForm.tsx similarity index 97% rename from apps/database-new/components/AssistantChatForm.tsx rename to packages/ui/src/patterns/AssistantChatForm.tsx index 7844ceaf017..ccbb469a67c 100644 --- a/apps/database-new/components/AssistantChatForm.tsx +++ b/packages/ui/src/patterns/AssistantChatForm.tsx @@ -1,7 +1,8 @@ import { Loader2 } from 'lucide-react' import React, { ChangeEvent, createRef, useEffect } from 'react' +import { TextArea } from '../components/shadcn/ui/text-area' +import { cn } from '../lib/utils' import { useFormStatus } from 'react-dom' -import { TextArea_Shadcn_, cn } from 'ui' export interface FormProps extends React.FormHTMLAttributes { loading?: boolean @@ -85,7 +86,7 @@ const AssistantChatForm = React.forwardRef( return (
-