mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
This reverts commit a2b01e0bc5.
This commit is contained in:
1 parent
d00b3d199f
commit
d6b7c1b7ba
22 files changed
+490
-686
No files matched your search
@@ -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' },
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 <></>
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>
|
||||
}
|
||||
+26
-17
@@ -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>
|
||||
|
||||
+21
-9
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
Vendored
-1
@@ -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.
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
}
|
||||
Generated
+4
-375
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+3
-2
@@ -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
|
||||
Reference in new issue
Block a user