Revert "feat: Add streaming responses to the new RLS Editor (#19304)" (#19439)

This reverts commit a2b01e0bc5.
This commit is contained in:
Kevin Grüneberg authored and GitHub committed 2023-12-05 16:50:26 +00:00
1 parent d00b3d199f
commit d6b7c1b7ba
22 files changed
+490 -686

No files matched your search

+1 -2
View File
@@ -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
@@ -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('')
@@ -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' },
}
)
}
}
-9
View File
@@ -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 <></>
}
-3
View File
@@ -1,3 +0,0 @@
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>
}
@@ -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<HTMLDivElement>(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<z.infer<typeof FormSchema>>({
@@ -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 (
<div id={'ai-chat-assistant'} className="flex flex-col h-full max-w-full">
<div id={'ai-chat-assistant'} className="flex flex-col h-full">
<div className="overflow-auto flex-1">
<Message
key="zero"
role="assistant"
content={`Hi${
name ? ' ' + name : ''
@@ -94,19 +101,21 @@ export const AIPolicyChat = ({
</div>
</Message>
{messages.map((m) => (
{sorted.map((m, idx) => (
<Message
key={`message-${m.id}`}
name={m.name}
key={`message-${idx}`}
name={name}
role={m.role}
content={m.content}
createdAt={new Date(m.createdAt || new Date()).getTime()}
isDebug={m.isDebug}
content={
m.content[0] && m.content[0].type === 'text' ? m.content[0].text.value : undefined
}
createdAt={m.created_at}
isDebug={(m.metadata as any).type === 'debug'}
onDiff={onDiff}
/>
))}
{pendingReply && <Message key="thinking" role="assistant" content="Thinking..." />}
{pendingReply && <Message role="assistant" content="Thinking..." />}
<div ref={bottomRef} className="h-1" />
</div>
@@ -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
}
@@ -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 (
<pre className="rounded-md relative group">
<CodeBlock
value={formatted}
language="sql"
className={cn(
'!bg-transparent !py-3 !px-3.5 prose dark:prose-dark',
// change the look of the code block. The flex hack is so that the code is wrapping since
// every word is a separate span
'[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap'
)}
hideCopy
hideLineNumbers
/>
<div className="absolute top-3 right-3 bg-surface-100 border-muted border rounded-lg h-[28px] hidden group-hover:block">
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button type="text" size="tiny" onClick={() => onDiff(formatted)}>
<FileDiff className="h-4 w-4" />
</Button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-alternative py-1 px-2 leading-none shadow',
'border border-background',
].join(' ')}
>
<span className="text-xs text-foreground">Apply changes</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button
type="text"
size="tiny"
onClick={() => navigator.clipboard.writeText(formatted).then()}
>
<Copy className="h-4 w-4" />
</Button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-alternative py-1 px-2 leading-none shadow',
'border border-background',
].join(' ')}
>
<span className="text-xs text-foreground">Copy code</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</div>
</pre>
)
}
@@ -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({
/>
</div>
)
}, [content, profile?.username, role])
}, [role])
if (!content) return null
return (
<div className="flex flex-col py-4 gap-4 border-t px-5 text-foreground-light text-sm">
<div className="flex flex-col py-4 gap-4 border-t px-5">
<div className="flex flex-row gap-3 items-center">
{icon}
<span className="text-sm">{role === 'assistant' ? 'Assistant' : name ? name : 'You'}</span>
{createdAt && (
<span className="text-xs text-foreground-muted">{dayjs(createdAt).fromNow()}</span>
<span className="text-xs text-foreground-muted">{dayjs(createdAt * 1000).fromNow()}</span>
)}
{isDebug && <Badge color="amber">Debug request</Badge>}
</div>
<ReactMarkdown
className="gap-2.5 flex flex-col"
remarkPlugins={[remarkGfm]}
components={{
...markdownComponents,
pre: (props: any) => {
return <AIPolicyPre onDiff={onDiff}>{props.children[0].props.children}</AIPolicyPre>
p: ({ children }) => <div className="text-foreground-light text-sm">{children}</div>,
// intentionally rendering as pre. The other approach would be to render as code element,
// but that will render <code> 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 (
<div
className="rounded-md border border-control overflow-hidden relative group"
key={key}
>
<CodeEditor
id={`rls-sql_${key}`}
language="pgsql"
className="h-48"
value={formatted}
autofocus={false}
options={{
scrollBeyondLastLine: false,
lineDecorationsWidth: 0,
lineNumbersMinChars: 3,
}}
/>
<div className="absolute top-3 right-3 bg-surface-100 border-muted border rounded-lg h-[28px] hidden group-hover:block">
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button type="text" size="tiny" onClick={() => onDiff(formatted)}>
<FileDiff className="h-4 w-4" />
</Button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-alternative py-1 px-2 leading-none shadow',
'border border-background',
].join(' ')}
>
<span className="text-xs text-foreground">Apply changes</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button
type="text"
size="tiny"
onClick={() => navigator.clipboard.writeText(formatted).then()}
>
<Copy className="h-4 w-4" />
</Button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-alternative py-1 px-2 leading-none shadow',
'border border-background',
].join(' ')}
>
<span className="text-xs text-foreground">Copy code</span>
</div>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</div>
</div>
)
},
}}
>
@@ -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<IStandaloneCodeEditor | null>(null)
const diffEditorRef = useRef<IStandaloneDiffEditor | null>(null)
const placeholder = generatePlaceholder(selectedPolicy)
@@ -63,13 +61,32 @@ export const AIPolicyEditorPanel = memo(function ({
const [error, setError] = useState<QueryResponseError>()
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<MessageWithDebug[]>([])
const [loading, setLoading] = useState(false)
const [keepPreviousData, setKeepPreviousData] = useState(false)
const [debugThread, setDebugThread] = useState<ThreadMessage[]>([])
const [assistantVisible, setAssistantPanel] = useState(false)
const [ids, setIds] = useState<{ threadId: string; runId: string } | undefined>(undefined)
const [isAssistantChatInputEmpty, setIsAssistantChatInputEmpty] = useState(true)
const [incomingChange, setIncomingChange] = useState<string | undefined>(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 ({
<div className={cn('flex border-l grow w-full', assistantVisible && 'w-[40%]')}>
<AIPolicyChat
messages={messages}
onSubmit={(message) =>
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'}
/>
</div>
)}
@@ -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<editor.IStandaloneCodeEditor>()
const editorRef = useRef<any>()
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 || <Connecting />}
options={optionsMerged}
+3
View File
@@ -0,0 +1,3 @@
export const aiKeys = {
rlsSuggest: (threadId: string, runId: string) => [threadId, runId] as const,
}
@@ -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<RlsSuggestResponse>(BASE_PATH + '/api/ai/sql/suggest', {
thread_id,
prompt,
entityDefinitions,
policyDefinition,
})
if (!isResponseOk(response)) {
throw response.error
}
return response
}
type RlsSuggestData = Awaited<ReturnType<typeof rlsSuggest>>
export const useRlsSuggestMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<RlsSuggestData, ResponseError, RlsSuggestVariables>,
'mutationFn'
> = {}) => {
return useMutation<RlsSuggestData, ResponseError, RlsSuggestVariables>(
(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,
}
)
}
+37
View File
@@ -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<ReturnType<typeof rlsSuggest>>
export type RlsSuggestDataError = ResponseError
export const useRlsSuggestQuery = <TData = RlsSuggestData>(
{ thread_id, run_id }: RlsSuggestVariables,
options: UseQueryOptions<RlsSuggestData, RlsSuggestDataError, TData> = {}
) =>
useQuery<RlsSuggestData, RlsSuggestDataError, TData>(
aiKeys.rlsSuggest(thread_id, run_id),
({ signal }) => rlsSuggest({ thread_id, run_id }, signal),
options
)
-1
View File
@@ -1,6 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
-1
View File
@@ -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",
@@ -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
@@ -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
+2 -7
View File
@@ -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"]
}
+4 -375
View File
@@ -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",
+1
View File
@@ -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
@@ -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<HTMLFormElement> {
loading?: boolean
@@ -85,7 +86,7 @@ const AssistantChatForm = React.forwardRef<HTMLFormElement, FormProps>(
return (
<form ref={ref} className="relative" {...props}>
<div className={cn('absolute', 'top-2 left-2', 'ml-1 w-6 h-6 rounded-full bg-dbnew')}></div>
<TextArea_Shadcn_
<TextArea
name="value"
ref={textAreaRef}
autoFocus