Chore/use completions api (#20246)

* Start

* Refactor code and remove unused imports

* Refactor SchemaFlowHandler and UserChat components

* Refactor code and remove unused files

* Refactor Thread component and remove CurrentThreadName import

* Remove oldest_messages view from supabase.ts

* Refactor supabase.ts file

* Hook up loading state ChatInput component and remove old route handlers

* Add updated prompt

* Refactor chat form component and remove unused code

* Make the suspense work when fetching messages.

* Small refactor in the chat assistant form component.

* Experimenting with streaming responses. WIP.

* Move all components to thread_id/message_id folder.

* Massive refactor but uses Nextjs app router properly.

* Add a conditional submit which is used if the user haven't been logged in.

* Add a typecheck command to db-new app.

* Minor fixes.

* Bunch of minor fixes.

* Clean up more code.

* Refactor the AssistantChatForm to use the new React forms features.

* Run fitView after 50 milliseconds because it didn't run in some cases.

* Style and flow nudges

* Prettier

* Delete old file

---------

Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
This commit is contained in:
Terry SuttonandIvan Vasilov authored and GitHub committed 2024-02-19 15:52:58 -03:30
1 parent 02e7e9cf87
commit b4fa734c85
58 files changed
+899 -1689

No files matched your search

+5 -76
View File
@@ -1,53 +1,10 @@
'use client'
import { CHAT_EXAMPLES } from '@/data/chat-examples'
import { AssistantChatForm } from '@/components/AssistantChatForm'
import { useAppStateSnapshot } from '@/lib/state'
import { createClient } from '@/lib/supabase/client'
import { ExternalLink } from 'lucide-react'
import { useState } from 'react'
import { useFormState } from 'react-dom'
import { createThread } from './actions'
import { AssistantChatForm } from '@/components/AssistantChatForm'
import { cn } from 'ui'
const suggestions = CHAT_EXAMPLES
const NewThreadInput = () => {
const [value, setValue] = useState(() => {
if (typeof window !== 'undefined') {
const localPrompt = localStorage.getItem('prompt')
if (localPrompt) {
localStorage.removeItem('prompt')
return localPrompt
}
}
return ''
})
const initialState = {
message: undefined,
success: undefined,
data: {
value,
runId: undefined,
threadId: undefined,
messageId: undefined,
},
}
const [state, formAction] = useFormState(createThread, initialState)
// useEffect(() => {
// if (state.success && state.data.threadId && state.data.runId && state.data.messageId) {
// console.log(state.data)
// const { threadId, runId, messageId } = state.data
// console.log('threadId', threadId)
// console.log('runId', runId)
// console.log('messageId', messageId)
// // router.push(`/${state.data.threadId}/${state.data.runId}/${state.data.messageId}`)
// }
// }, [state.success])
const supabase = createClient()
const snap = useAppStateSnapshot()
@@ -55,48 +12,20 @@ const NewThreadInput = () => {
<>
<div className="relative w-10/12 xl:w-11/12 max-w-xl">
<AssistantChatForm
action={formAction}
key={'new-thread-form'}
id={'new-thread-form'}
onSubmit={async (event) => {
chatContext={'new'}
canSubmit={async () => {
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) {
event.preventDefault()
localStorage.setItem('prompt', value)
snap.setLoginDialogOpen(true)
return
return false
}
return true
}}
value={value}
placeholder="e.g Create a Telegram-like chat application"
onValueChange={(e) => setValue(e.target.value)}
message={state?.message}
/>
{state?.message && <p>{state?.message}</p>}
</div>
<div className="flex gap-3">
{suggestions.map((suggestion, idx) => (
<button
key={idx}
type="button"
className={cn(
'text-xs',
'flex items-center gap-3 !pr-3',
'transition border rounded-full px-3 py-1.5',
'text-light',
'hover:border-stronger hover:text'
)}
onClick={(event) => {
setValue(suggestion.prompt)
event.preventDefault()
}}
>
{suggestion.label}
<ExternalLink size={12} />
</button>
))}
</div>
</>
)
@@ -1,36 +0,0 @@
import { Suspense } from 'react'
import { ScrollArea } from 'ui'
import { cn } from '@ui/lib/utils/cn'
import { Messages } from './Messages'
import { ChatInput } from './ChatInput'
import { BottomMarker } from './BottomMarker'
async function Chat({ params }: { params: { threadId: string; runId: string } }) {
return (
<div
className={cn(
'bg',
'h-full',
'border-t xl:border-t-0 xl:border-r relative',
'flex flex-col h-full border-r',
'w-full xl:w-[400px] 2xl:w-[500px]'
)}
>
<div className="flex flex-col grow items-between">
<ScrollArea className="grow h-px">
<div className="flex flex-col py-2 xl:py-6">
<Suspense fallback={<p> loading</p>}>
<Messages params={params} />
</Suspense>
<BottomMarker />
</div>
</ScrollArea>
<ChatInput params={params} />
</div>
</div>
)
}
export { Chat }
@@ -1,63 +0,0 @@
'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'
function ChatInput({ params }: { params: { threadId: string; runId: string } }) {
const [value, setValue] = useState('')
const { threadId, runId } = params
const initialState = {
message: undefined,
success: undefined,
data: {
value,
},
}
const [state, formAction] = useFormState(updateThread, initialState)
const Message = () => {
const { pending } = useFormStatus()
console.log('state.message', state.message)
return !state.success && state.success !== undefined && !pending ? (
<p className="text-xs text-warning text-center w-full p-3">{state.message}</p>
) : null
}
return (
<div className="px-4 pb-4">
<AssistantChatForm
action={formAction}
key={`chat-thread-form-${runId}`}
id={`chat-thread-form-${runId}`}
value={value}
placeholder={
// loading
// ? 'Generating reply to request...'
// :
'Any changes to make?'
}
onValueChange={(v) => setValue(v.target.value)}
message={state.message}
>
<Input
type="hidden"
name="threadId"
value={threadId}
key={`chat-thread-form-${threadId}`}
/>
<Input type="hidden" name="runId" value={runId} key={`chat-thread-form-${runId}}`} />
</AssistantChatForm>
<Message />
</div>
)
}
export { ChatInput }
@@ -1,56 +0,0 @@
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { sortBy } from 'lodash'
import OpenAI from 'openai'
import UserChat from './UserChat'
dayjs.extend(relativeTime)
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
async function Messages({ params }: { params: { threadId: string; runId: string } }) {
const [run, { data: messages }] = await Promise.all([
openai.beta.threads.runs.retrieve(params.threadId, params.runId),
openai.beta.threads.messages.list(params.threadId),
])
const messagesSorted = sortBy(messages, (m) => m.created_at)
const userMessages = messagesSorted.filter((message) => message.role === 'user')
return (
<div className="flex flex-col py-2 xl:py-6">
{userMessages.map((message, idx) => {
const index = messages.indexOf(message)
const reply = messages[index + 1]
const isLatest = idx === userMessages.length - 1
const hoursFromNow = dayjs().diff(dayjs(message.created_at * 1000), 'hours')
const formattedTimeFromNow = dayjs(message.created_at * 1000).fromNow()
const formattedCreatedAt = dayjs(message.created_at * 1000).format('DD MMM YYYY, HH:mm')
const replyDuration =
reply !== undefined ? reply.created_at - message.created_at : undefined
const times = {
hoursFromNow,
formattedTimeFromNow,
formattedCreatedAt,
replyDuration,
}
return (
<UserChat
key={message.id}
message={message}
run={run}
isLatest={isLatest}
times={times}
/>
)
})}
</div>
)
}
export { Messages }
@@ -1,50 +0,0 @@
import { format } from 'sql-formatter'
import { getThread } from './MessageId.utils'
import { MonacoEditor } from './MonacoEditor'
import { CodeEditorContainer } from './CodeEditorContainer'
import OpenAI from 'openai'
const openai = new OpenAI()
async function waitForRunCompletion(params: { threadId: string; runId: string }) {
console.log('waiting for run completion')
let realRun = await openai.beta.threads.runs.retrieve(params.threadId, params.runId)
while (realRun.status === 'in_progress') {
//console.log('while..')
await new Promise((resolve) => setTimeout(resolve, 2000))
realRun = await openai.beta.threads.runs.retrieve(params.threadId, params.runId)
//console.log('realRun', realRun.status)
}
//console.log('Run completed:', realRun.status)
}
export async function CodeEditor({ params }: { params: any }) {
const { threadId, runId, messageId } = params
await waitForRunCompletion(params)
const content = await getThread({ threadId, runId, messageId })
const code = format(content, { language: 'postgresql' })
// keep polling until the run is completed
// useEffect(() => {
// snap.setSelectedCode(code)
// }, [code])
/**
* - CodeEditorContainer is a client component, which uses valtio state
* - MonacoEditor is a server component injected into it
*/
return (
<CodeEditorContainer>
<MonacoEditor id="sql-editor" language="pgsql" value={code} />
</CodeEditorContainer>
)
}
@@ -1,66 +0,0 @@
'use server'
import OpenAI from 'openai'
import { AssistantMessage } from '@/lib/types'
import { compact, sortBy } from 'lodash'
const openai = new OpenAI()
async function getThread({
threadId,
runId,
messageId,
}: {
threadId: string
runId: string
messageId: string
}) {
const [run, { data: _messages }] = await Promise.all([
openai.beta.threads.runs.retrieve(threadId, runId),
openai.beta.threads.messages.list(threadId),
])
let messages = compact(
await Promise.all(
_messages.map(async (m) => {
if (m.role === 'user' && m.content[0].type === 'text') {
return {
id: m.id,
role: 'user' as const,
created_at: m.created_at,
text: m.content[0].text.value,
}
}
if (m.content.length >= 1 && m.content[0].type === 'text') {
let sql = ''
if (m.content[0].type === 'text') {
sql = m.content[0].text.value.replaceAll('\n', '')
}
return {
id: m.id,
role: 'assistant' as const,
created_at: m.created_at,
sql,
}
}
})
)
)
messages = sortBy(messages, (m) => m.created_at)
const userMessages = messages.filter((m) => m.role === 'user')
const selectedMessageIdx = messages.findIndex((m) => m.id === messageId)
const selectedMessageReply = (
selectedMessageIdx !== -1 ? messages[selectedMessageIdx + 1] : undefined
) as AssistantMessage | undefined
const content = selectedMessageReply?.sql.replaceAll('```sql', '').replaceAll('```', '') || ''
return content
}
export { getThread }
@@ -1,97 +0,0 @@
import { format } from 'sql-formatter'
import { getThread } from './MessageId.utils'
import { parseTables } from '@/lib/utils'
import SchemaFlowHandler from './SchemaFlowHandler'
import OpenAI from 'openai'
import { createClient } from '@/lib/supabase/server'
import { cookies } from 'next/headers'
const openai = new OpenAI()
async function waitForRunCompletion(params: {
threadId: string
runId: string
messageId: string
}) {
console.log('Waiting for run completion')
let realRun = await openai.beta.threads.runs.retrieve(params.threadId, params.runId)
if (realRun.status !== 'in_progress') {
console.log('Run is not in progress. Exiting without running anything.')
// Check if it exists in Supabase DB
try {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const { data, error } = await supabase
.from('responses_ai')
.select('*')
.eq('message_id', params.messageId)
if (error) throw error
if (data.length > 0) {
return { newMessage: false }
}
} catch (error) {
console.error('The error is', error)
}
}
while (realRun.status === 'in_progress') {
await new Promise((resolve) => setTimeout(resolve, 2000))
realRun = await openai.beta.threads.runs.retrieve(params.threadId, params.runId)
}
console.log('Run completed:', realRun.status)
return { newMessage: true }
}
export async function SchemaFlow({ params }: { params: any }) {
//console.log('schemaFlow refetching')
const { threadId, runId, messageId } = params
// keep polling until the run is completed
const { newMessage } = await waitForRunCompletion(params)
const content = await getThread({ threadId, runId, messageId })
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const {
data: { user },
error: userError,
} = await supabase.auth.getUser()
if (!user) return
if (!newMessage) return
try {
const { error } = await supabase.from('responses_ai').insert({
message_id: messageId,
thread_id: threadId,
text: content,
run_id: runId,
user_id: user.id,
})
if (error) throw error
} catch (error) {
console.error(error)
}
console.log({ content })
//const code = format(content, { language: 'postgresql' })
//const tables = await parseTables(content)
// useEffect(() => {
// snap.setSelectedCode(code)
// }, [code])
return <SchemaFlowHandler content={content} />
}
@@ -1,29 +0,0 @@
import { CodeEditor } from '@/app/[threadId]/[runId]/[messageId]/CodeEditor'
import { SchemaFlow } from '@/app/[threadId]/[runId]/[messageId]/SchemaFlow'
import GraphLoader from '@/components/Loaders/GraphLoader'
import SchemaLoader from '@/components/Loaders/SchemaLoader'
import { Loader2 } from 'lucide-react'
import Image from 'next/image'
import { Suspense } from 'react'
interface ThreadPageProps {
params: {
threadId: string
runId: string
messageId: string
}
}
export default async function ThreadPage({ params }: ThreadPageProps) {
return (
<div className="grow max-h-screen flex flex-row items-center justify-between bg-alternative h-full">
<Suspense fallback={<SchemaLoader />}>
<SchemaFlow params={params} />
</Suspense>
<Suspense fallback={<GraphLoader />}>
<CodeEditor params={params} />
</Suspense>
</div>
)
}
@@ -1,22 +0,0 @@
import { Chat } from '@/app/[threadId]/[runId]/Chat'
import SaveSchemaDropdown from '@/components/Header/SaveSchemaDropdown'
import ToggleCodeEditorButton from '@/components/Header/ToggleCodeEditorButton'
export default function Layout({
children,
params,
}: {
children: React.ReactNode
params: { threadId: string; runId: string }
}) {
return (
<div className="flex flex-col-reverse items-between xl:flex-row xl:items-center xl:justify-between bg-alternative h-full">
<Chat params={params} />
<div className="xl:hidden flex items-center gap-x-2 justify-end border-t py-2 px-2 bg-background">
<ToggleCodeEditorButton />
<SaveSchemaDropdown />
</div>
{children}
</div>
)
}
@@ -1,74 +0,0 @@
import * as React from 'react'
const RunIdPage = async () => {
return <></>
}
import OpenAI from 'openai'
import { AssistantMessage } from '@/lib/types'
import { compact, sortBy } from 'lodash'
const openai = new OpenAI()
async function getThread({
threadId,
runId,
messageId,
}: {
threadId: string
runId: string
messageId: string
}) {
console.log('getting thread..', threadId, runId, messageId)
const [run, { data: _messages }] = await Promise.all([
openai.beta.threads.runs.retrieve(threadId, runId),
openai.beta.threads.messages.list(threadId),
])
let messages = compact(
await Promise.all(
_messages.map(async (m) => {
if (m.role === 'user' && m.content[0].type === 'text') {
return {
id: m.id,
role: 'user' as const,
created_at: m.created_at,
text: m.content[0].text.value,
}
}
if (m.content.length >= 1 && m.content[0].type === 'text') {
let sql = ''
if (m.content[0].type === 'text') {
sql = m.content[0].text.value.replaceAll('\n', '')
}
return {
id: m.id,
role: 'assistant' as const,
created_at: m.created_at,
sql,
}
}
})
)
)
messages = sortBy(messages, (m) => m.created_at)
// const userMessages = messages.filter((m) => m.role === 'user')
const selectedMessageIdx = messages.findIndex((m) => m.id === messageId)
const selectedMessageReply = (
selectedMessageIdx !== -1 ? messages[selectedMessageIdx + 1] : undefined
) as AssistantMessage | undefined
const content = selectedMessageReply?.sql.replaceAll('```sql', '').replaceAll('```', '') || ''
return content
}
export { getThread }
export default RunIdPage
@@ -0,0 +1,23 @@
import { format } from 'sql-formatter'
import { MonacoEditor } from '@/components/MonacoEditor'
import { CodeEditorContainer } from './CodeEditorContainer'
export async function CodeEditor({ promisedMessage }: { promisedMessage: Promise<string> }) {
const code = await promisedMessage
// strip the ```sql ``` from the code
// not sure why we need this & can't get openai to stop returning it
const strippedCode = code.replace('```sql\n', '').replace('\n```', '')
const formattedCode = format(strippedCode, { language: 'postgresql' })
/**
* - CodeEditorContainer is a client component, which uses valtio state
* - MonacoEditor is a server component injected into it
*/
return (
<CodeEditorContainer>
<MonacoEditor id="sql-editor" language="pgsql" value={formattedCode} />
</CodeEditorContainer>
)
}
@@ -1,61 +1,30 @@
'use client'
import { useAppStateSnapshot } from '@/lib/state'
import { pull } from 'lodash'
import { Message as MessageItem } from 'ai/react'
import Link from 'next/link'
import { useParams, usePathname, useRouter } from 'next/navigation'
import OpenAI from 'openai'
import { useEffect } from 'react'
import { useParams, usePathname } from 'next/navigation'
import { cn } from 'ui'
interface UserChatProps {
message: OpenAI.Beta.Threads.Messages.ThreadMessage
interface MessageItemProps {
message: MessageItem
isLatest: boolean
times: {
hoursFromNow: number
formattedTimeFromNow: string
formattedCreatedAt: string
replyDuration: number | undefined
}
run: OpenAI.Beta.Threads.Run
}
const UserChat = ({ message, isLatest, times, run }: UserChatProps) => {
const router = useRouter()
const snap = useAppStateSnapshot()
const { threadId, runId } = useParams()
// console.log(run)
const LOADING_STATUSES = ['in_progress', 'queued']
const runIsInProgressRemotely = LOADING_STATUSES.includes(run.status)
useEffect(() => {
if (runIsInProgressRemotely) {
// set a local state for run loading
// this state will be updated via other client components when completing a run
// remove current message id from array if it exists
let currentRunsLoading = [...snap.runsLoading]
pull(currentRunsLoading, run.id)
const payload = [...currentRunsLoading, run.id]
snap.setRunsLoading([...payload])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [runIsInProgressRemotely, run.id]) // Intentionally left snap out of the dependency array
const MessageItem = ({ message, isLatest, times }: MessageItemProps) => {
const { thread_id } = useParams()
// using the local state for run loading
const isLoading = snap.runsLoading.includes(run.id) && isLatest
const isLoading = false
const { hoursFromNow, formattedTimeFromNow, formattedCreatedAt, replyDuration } = times
const { hoursFromNow, formattedTimeFromNow, formattedCreatedAt } = times
// chat shown as selected when url matches
const isSelected = usePathname().includes(message.id)
// extract the text from the assistant message
const message_content = message.content[0]
const text = message_content.type === 'text' ? message_content.text.value : ''
return (
<Link
className={cn(
@@ -64,7 +33,7 @@ const UserChat = ({ message, isLatest, times, run }: UserChatProps) => {
isSelected && 'bg-surface-200',
isSelected ? 'border-r-foreground' : 'border-r border-r-transparent'
)}
href={`/${threadId}/${runId}/${message.id}`}
href={`/${thread_id}/${message.id}`}
>
<div className="flex flex-col justify-between items-center relative top-3">
<div
@@ -75,6 +44,7 @@ const UserChat = ({ message, isLatest, times, run }: UserChatProps) => {
: 'bg-transparent border-foreground-muted group-hover:border-foreground'
)}
/>
{isLoading && (
<span
className={cn(
@@ -86,6 +56,7 @@ const UserChat = ({ message, isLatest, times, run }: UserChatProps) => {
<div className="absolute border w-4 h-4 rounded-full z-0" />
</span>
)}
{/* Node line*/}
{!isLatest && <div className="border-l border-strong flex-grow" />}
</div>
@@ -106,7 +77,7 @@ const UserChat = ({ message, isLatest, times, run }: UserChatProps) => {
</svg>
</span>
<div
title={text}
title={message.content}
className={cn(
'cursor-pointer transition relative overflow-hidden',
'w-full rounded-lg rounded-tl-none',
@@ -121,9 +92,9 @@ const UserChat = ({ message, isLatest, times, run }: UserChatProps) => {
isSelected ? 'text-foreground' : 'text-light group-hover:text-foreground'
)}
>
{text}
{message.content}
</p>
{/* {isLoading && <div className="chat-shimmering-loader w-full h-0.5 absolute bottom-0" />} */}
{isLoading && <div className="chat-shimmering-loader w-full h-0.5 absolute bottom-0" />}
</div>
</div>
{isSelected && (
@@ -135,11 +106,6 @@ const UserChat = ({ message, isLatest, times, run }: UserChatProps) => {
)}
>
Sent {hoursFromNow > 6 ? `on ${formattedCreatedAt}` : formattedTimeFromNow}
{replyDuration !== undefined
? ` with ${replyDuration}s response`
: isLoading
? ', generating response...'
: ''}
</p>
)}
</div>
@@ -147,4 +113,4 @@ const UserChat = ({ message, isLatest, times, run }: UserChatProps) => {
)
}
export default UserChat
export default MessageItem
@@ -0,0 +1,62 @@
import { ScrollArea } from 'ui'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { BottomMarker } from './BottomMarker'
import { getMessages } from './getMessages'
import MessageItem from './MessageItem'
dayjs.extend(relativeTime)
export const Messages = async ({ threadId }: { threadId: string }) => {
const { data: messages, error } = await getMessages(threadId)
if (error) {
return <>Error happened</>
}
const initialMessages = messages.flatMap((m) => [
{
id: m.message_id,
content: m.message_input,
role: 'user' as const,
createdAt: new Date(m.created_at),
},
{
id: m.message_id,
content: m.message_content,
role: 'assistant' as const,
createdAt: new Date(m.created_at),
},
])
const userMessages = initialMessages.filter((m) => m.role === 'user')
return (
<ScrollArea className="grow h-px">
<div className="flex flex-col mb-6 xl:mb-12">
{userMessages.map((message, idx) => {
const createdAtTimestamp = dayjs(message.createdAt)
const isLatest = Array.isArray(messages) && idx === messages.length - 1
const hoursFromNow = dayjs().diff(createdAtTimestamp, 'hour')
const formattedTimeFromNow = dayjs(createdAtTimestamp).fromNow()
const formattedCreatedAt = dayjs(createdAtTimestamp).format('DD MMM YYYY, HH:mm')
const times = {
hoursFromNow,
formattedTimeFromNow,
formattedCreatedAt,
}
return (
<MessageItem key={message.id} message={message} isLatest={isLatest} times={times} />
)
})}
</div>
<BottomMarker />
</ScrollArea>
)
}
@@ -0,0 +1,8 @@
import SchemaFlowHandler from '@/components/SchemaFlowHandler'
export async function SchemaFlow({ promisedMessage }: { promisedMessage: Promise<string> }) {
const code = await promisedMessage
const strippedCode = code.replace('```sql\n', '').replace('\n```', '')
return <SchemaFlowHandler content={strippedCode} />
}
@@ -0,0 +1,135 @@
import { stripIndent } from 'common-tags'
import { cookies } from 'next/headers'
import OpenAI from 'openai'
// import is weird, what's up with this?
import { createClient } from '@/lib/supabase/server'
import { ContextLengthError } from '../../../../../packages/ai-commands/src/errors'
import { getMessages } from './getMessages'
// Create an OpenAI API client (that's edge friendly!)
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
})
// Set the runtime to edge for best performance
export const runtime = 'edge'
export type AiAssistantMessage = {
content: string
role: 'user' | 'assistant'
}
/**
* Get the latest AI generated message. In the DB, there's a pair of user prompt and AI response, so if the
* user sent a message but that message lacks the corresponding AI response, a call will be made to OpenAI to generate
* it.
*/
export const getAssistantResponse = async (threadId: string, messageId: string) => {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const { data: message, error } = await supabase
.from('messages')
.select()
.eq('message_id', messageId)
.single()
if (!message || error) {
throw new Error('A message with that id does not exist.')
}
// if the message lacks the AI response, fetch it from OpenAI
if (!message.message_content) {
const { data: messages, error } = await getMessages(threadId)
if (error) {
throw new Error('Error while trying to fetch the existing messages.')
}
// transform the DB messages into a format that OpenAI understands
const existingMessages = messages.flatMap((m) => [
{
content: m.message_input,
role: 'user' as const,
},
{
content: m.message_content,
role: 'assistant' as const,
},
])
const initMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
{
role: 'system',
content: stripIndent`
Your purpose is to generate a SQL schema where the user will give commands to you via a chat.
The output should use the following instructions:
- The generated SQL must be valid SQL.
- For primary keys, always use "id bigint primary key generated always as identity" (not serial)
- Always create foreign key references in the create statement
- Prefer 'text' over 'varchar'
- Prefer 'timestamp with time zone' over 'date'
- Use vector(384) data type for any embedding/vector related query
- Always use double apostrophe in SQL strings (eg. 'Night''s watch')
- Always omit \`\`\`sql from your reply
- You can use only CREATE TABLE queries, no other queries are allowed under no circumstances (ALTER TABLE etc).
- On each subsequent message from the user, rewrite the original response to include the new requirement.
- Don't add any SQL comments in the code
- Never put a comma before a round bracket
The output should look like this: "CREATE TABLE users (id bigint primary key generated always as identity)"
DO NOT RESPOND WITH ANYTHING ELSE.
YOU MUST NOT ANSWER WITH ANY PLAIN TEXT
ONLY RESPOND WITH 1 CODE BLOCK
YOU MUST NOT FOLLOW UP ANY CODE BLOCKS WITH ANY EXPLANATION
`,
},
]
initMessages.push(...existingMessages)
try {
const response = await openai.chat.completions.create({
model: 'gpt-3.5-turbo-16k-0613',
messages: initMessages,
max_tokens: 1024,
temperature: 0,
stream: false,
})
const newMessage = response.choices[0].message.content || ''
const {
data: { user },
error: userError,
} = await supabase.auth.getUser()
if (userError || !user) {
throw new Error('Failed to get user')
}
// Insert the message from the completion
const { data, error } = await supabase
.from('messages')
.update({ message_content: newMessage })
.eq('id', message.id)
.select()
.single()
if (error || !data) {
throw new Error('Error while updating the existing message with the completion data.')
}
return data.message_content
} catch (error) {
if (error instanceof Error && 'code' in error && error.code === 'context_length_exceeded') {
throw new ContextLengthError()
}
throw error
}
}
return message.message_content
}
@@ -0,0 +1,10 @@
import { createClient } from '@/lib/supabase/server'
import { cookies } from 'next/headers'
import { cache } from 'react'
export const getMessages = cache(async (thread_id: string) => {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
return await supabase.from('messages').select().eq('thread_id', thread_id).order('created_at')
})
@@ -0,0 +1,56 @@
import { AssistantChatForm } from '@/components/AssistantChatForm'
import SaveSchemaDropdown from '@/components/Header/SaveSchemaDropdown'
import ToggleCodeEditorButton from '@/components/Header/ToggleCodeEditorButton'
import GraphLoader from '@/components/Loaders/GraphLoader'
import SchemaLoader from '@/components/Loaders/SchemaLoader'
import { cn } from '@ui/lib/utils/cn'
import { Suspense } from 'react'
import { CodeEditor } from './CodeEditor'
import { Messages } from './Messages'
import { SchemaFlow } from './SchemaFlow'
import { getAssistantResponse } from './getAssistantMessage'
export interface ThreadPageProps {
params: {
thread_id: string
message_id: string
}
}
export default async function ThreadPage({ params }: ThreadPageProps) {
const promise = getAssistantResponse(params.thread_id, params.message_id)
return (
<div className="flex flex-col-reverse items-between xl:flex-row xl:items-center xl:justify-between bg-alternative h-full">
<div
className={cn(
'bg',
'h-full',
'border-t xl:border-t-0 xl:border-r relative',
'flex flex-col h-full border-r',
'w-full xl:w-[400px] 2xl:w-[500px]'
)}
>
<div className="flex flex-col grow items-between">
<Messages threadId={params.thread_id} />
<div className="px-4 pb-4">
<AssistantChatForm chatContext={'edit'} placeholder={'Any changes to make?'} />
</div>
</div>
</div>
<div className="xl:hidden flex items-center gap-x-2 justify-end border-t py-2 px-2 bg-background">
<ToggleCodeEditorButton />
<SaveSchemaDropdown />
</div>
<div className="grow max-h-screen flex flex-row items-center justify-between bg-alternative h-full">
<Suspense fallback={<SchemaLoader />}>
<SchemaFlow promisedMessage={promise} />
</Suspense>
<Suspense fallback={<GraphLoader />}>
<CodeEditor promisedMessage={promise} />
</Suspense>
</div>
</div>
)
}
+151 -205
View File
@@ -1,21 +1,12 @@
'use server'
import { last, sortBy } from 'lodash'
import { AssistantMessage } from '@/lib/types'
import { parseTables } from '@/lib/utils'
import { createClient } from '@/lib/supabase/server'
import { Message } from 'ai'
import { revalidatePath } from 'next/cache'
import { cookies } from 'next/headers'
import { RedirectType, redirect } from 'next/navigation'
import { redirect } from 'next/navigation'
import OpenAI from 'openai'
import { z } from 'zod'
import { threadId } from 'worker_threads'
import { MessageContentText } from 'openai/resources/beta/threads/index.mjs'
const openai = new OpenAI()
export async function logout() {
const cookieStore = cookies()
@@ -32,6 +23,151 @@ export async function logout() {
redirect('/')
}
//export async function createThread(prevState: any, formData: FormData) {
export async function createThread(input: string, message: Message, currentThread?: string) {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
let thread_id = currentThread ?? ''
let message_id = ''
try {
const {
data: { user },
error: userError,
} = await supabase.auth.getUser()
if (userError || !user) {
return {
success: false,
message: 'Failed to get user',
data: undefined,
}
}
// create a new thread
if (!thread_id) {
try {
const { data, error } = await supabase
.from('threads')
.insert({ user_id: user.id, thread_title: input })
.select()
if (error) throw error
if (data) {
console.log({ data })
thread_id = data[0].id
// message_id = data[0].message_id
}
} catch (error) {
console.error(error)
}
}
//insert the message from the completion
try {
const { data, error } = await supabase
.from('messages')
.insert({
thread_id,
message_content: message.content,
message_input: input,
message_role: 'assistant',
user_id: user.id,
})
.select()
if (error) throw error
if (data) {
message_id = data[0].message_id
}
} catch (error) {
console.error(error)
}
} catch (error: any) {
console.error(error)
return {
success: false,
message: 'Failed to update title to update title',
data: undefined,
}
}
redirect(`/${thread_id}/${message_id}`)
}
export async function upsertThreadMessage(input: string, currentThread?: string) {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
let thread_id = currentThread ?? ''
let message_id = ''
try {
const {
data: { user },
error: userError,
} = await supabase.auth.getUser()
if (userError || !user) {
return {
success: false,
message: 'Failed to get user',
data: undefined,
}
}
// create a new thread
if (!thread_id) {
try {
const { data, error } = await supabase
.from('threads')
.insert({ user_id: user.id, thread_title: input })
.select()
if (error) throw error
if (data) {
console.log({ data })
thread_id = data[0].id
// message_id = data[0].message_id
}
} catch (error) {
console.error(error)
}
}
//insert the message from the completion
try {
const { data, error } = await supabase
.from('messages')
.insert({
thread_id,
message_content: '',
message_input: input,
message_role: 'assistant',
user_id: user.id,
})
.select()
if (error) throw error
if (data) {
message_id = data[0].message_id
}
} catch (error) {
console.error(error)
}
} catch (error: any) {
console.error(error)
return {
success: false,
message: 'Failed to update title to update title',
data: undefined,
}
}
redirect(`/${thread_id}/${message_id}`)
}
export async function deleteThread(prevState: any, formData: FormData) {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
@@ -45,8 +181,7 @@ export async function deleteThread(prevState: any, formData: FormData) {
thread_id: formData.get('thread_id'),
})
await supabase.from('threads').delete().eq('thread_id', data.thread_id)
await openai.beta.threads.del(data.thread_id)
await supabase.from('threads').delete().eq('id', data.thread_id)
revalidatePath('/profile')
@@ -71,18 +206,18 @@ export async function updateThreadName(prevState: any, formData: FormData) {
try {
const schema = z.object({
thread_title: z.string(),
row_id: z.string(),
thread_id: z.string(),
})
const data = schema.parse({
thread_title: formData.get('thread_title'),
row_id: formData.get('row_id'),
thread_id: formData.get('thread_id'),
})
const { error } = await supabase
.from('threads')
.update({ thread_title: data.thread_title })
.eq('id', data.row_id)
.eq('id', data.thread_id)
if (error) {
throw error
}
@@ -102,192 +237,3 @@ export async function updateThreadName(prevState: any, formData: FormData) {
}
}
}
export async function createThread(prevState: any, formData: FormData) {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
let redirectUrl = ''
try {
const schema = z.object({
value: z.string(),
})
const data = schema.parse({
value: formData.get('value'),
})
const {
data: { user },
error: userError,
} = await supabase.auth.getUser()
if (userError || !user) {
return {
success: false,
message: 'Failed to get user',
data: undefined,
}
}
const thread = await openai.beta.threads.create()
const message = await openai.beta.threads.messages.create(thread.id, {
role: 'user',
content: data.value,
})
const createRun = await openai.beta.threads.runs.create(thread.id, {
assistant_id: 'asst_oLWrK8lScZVNEpfjwUIvBAnq',
})
const [run, { data: messages }] = await Promise.all([
openai.beta.threads.runs.retrieve(thread.id, createRun.id),
openai.beta.threads.messages.list(thread.id),
])
const threadTitle = messages
.filter((m) => m.role === 'user' && m.content[0]?.type === 'text')
.map((m) => {
if (m.content[0]?.type === 'text') {
return m.content[0]?.text?.value
}
return undefined
})
.find((text) => text !== undefined)
try {
const { error } = await supabase.from('threads').insert({
thread_id: thread.id,
run_id: run.id,
user_id: user.id,
thread_title: threadTitle as string,
latest_message_id: message.id,
})
revalidatePath('/profile')
if (error) throw error
} catch (error) {
console.error(error)
}
// insert into supabase
try {
const { error } = await supabase.from('messages_user').insert({
message_id: message.id,
thread_id: thread.id,
text: (message.content[0] as MessageContentText).text.value,
run_id: run.id,
user_id: user.id,
})
if (error) throw error
} catch (error) {
console.error(error)
}
redirectUrl = `/${thread.id}/${run.id}/${message.id}`
// return {
// success: true,
// message: 'Managed to run action',
// data: {
// messageId: message.id,
// runId: run.id,
// threadId: thread.id,
// },
// }
} catch (error: any) {
console.error(error)
return {
success: false,
message: 'Failed to update title to update title',
data: undefined,
}
}
redirect(redirectUrl)
}
export async function updateThread(prevState: any, formData: FormData) {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
let redirectUrl = ''
try {
const schema = z.object({
value: z.string(),
threadId: z.string(),
runId: z.string(),
})
const data = schema.parse({
value: formData.get('value'),
threadId: formData.get('threadId'),
runId: formData.get('runId'),
})
const message = await openai.beta.threads.messages.create(data.threadId, {
content: data.value,
role: 'user',
})
revalidatePath(`/${data.threadId}/${data.runId}`, 'layout')
// insert into supabase
const {
data: { user },
error: userError,
} = await supabase.auth.getUser()
if (userError || !user) {
return {
success: false,
message: 'Failed to get user',
data: undefined,
}
}
console.log('message.content[0]', message.content[0])
try {
const { error } = await supabase.from('messages_user').insert({
message_id: message.id,
thread_id: data.threadId,
text: (message.content[0] as MessageContentText).text.value,
run_id: data.runId,
user_id: user.id,
})
if (error) throw error
} catch (error) {
console.error(error)
}
console.log('message.id', message.id)
// update existing thread with latest messageID
try {
const { error } = await supabase
.from('threads')
.update({
latest_message_id: message.id,
})
.eq('thread_id', data.threadId)
if (error) throw error
} catch (error) {
console.error('Update error:', error)
}
const run = await openai.beta.threads.runs.create(message.thread_id, {
assistant_id: 'asst_oLWrK8lScZVNEpfjwUIvBAnq',
})
revalidatePath(`/${data.threadId}/${data.runId}/${message.id}`, 'layout')
redirectUrl = `/${message.thread_id}/${run.id}/${message.id}`
} catch (error: any) {
return {
success: false,
message: 'Failed to update schema with new prompt',
data: undefined,
}
}
revalidatePath(redirectUrl)
redirect(redirectUrl)
}
@@ -1,13 +0,0 @@
import OpenAI from 'openai'
const openai = new OpenAI()
export async function POST(req: Request, { params }: { params: { threadId: string } }) {
if (!req.body) {
return Response.error()
}
const kill = await openai.beta.threads.del(params.threadId)
console.log({ kill })
return Response.json({})
}
@@ -1,50 +0,0 @@
// import { parseTables } from '@/lib/utils'
import { compact } from 'lodash'
import OpenAI from 'openai'
const openai = new OpenAI()
export async function GET(
req: Request,
{ params }: { params: { threadId: string; runId: string } }
) {
const [run, { data: messages }] = await Promise.all([
openai.beta.threads.runs.retrieve(params.threadId, params.runId),
openai.beta.threads.messages.list(params.threadId),
])
const mappedMessages = compact(
await Promise.all(
messages.map(async (m) => {
if (m.role === 'user' && m.content[0].type === 'text') {
return {
id: m.id,
role: 'user' as const,
created_at: m.created_at,
text: m.content[0].text.value,
}
}
if (m.content.length >= 1 && m.content[0].type === 'text') {
let sql = ''
if (m.content[0].type === 'text') {
sql = m.content[0].text.value.replaceAll('\n', '')
}
return {
id: m.id,
role: 'assistant' as const,
created_at: m.created_at,
sql,
}
}
})
)
)
const result = {
id: params.threadId,
status: run.status === 'completed' ? 'completed' : 'loading',
messages: mappedMessages,
}
return Response.json(result)
}
@@ -1,22 +0,0 @@
import OpenAI from 'openai'
const openai = new OpenAI()
export async function POST(req: Request, { params }: { params: { threadId: string } }) {
if (!req.body) {
return Response.error()
}
const { prompt } = await req.json()
const message = await openai.beta.threads.messages.create(params.threadId, {
content: prompt,
role: 'user',
})
const run = await openai.beta.threads.runs.create(message.thread_id, {
assistant_id: 'asst_oLWrK8lScZVNEpfjwUIvBAnq',
})
return Response.json({ threadId: message.thread_id, runId: run.id })
}
@@ -1,58 +0,0 @@
import { cookies } from 'next/headers'
import { createClient } from '@/lib/supabase/server'
import OpenAI from 'openai'
import { revalidatePath } from 'next/cache'
const openai = new OpenAI()
export async function POST(req: Request) {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
if (!req.body) {
return Response.error()
}
const { prompt, userID } = await req.json()
const thread = await openai.beta.threads.create()
await openai.beta.threads.messages.create(thread.id, {
role: 'user',
content: prompt,
})
const createRun = await openai.beta.threads.runs.create(thread.id, {
assistant_id: 'asst_oLWrK8lScZVNEpfjwUIvBAnq',
})
const [run, { data: messages }] = await Promise.all([
openai.beta.threads.runs.retrieve(thread.id, createRun.id),
openai.beta.threads.messages.list(thread.id),
])
const threadTitle = messages
.filter((m) => m.role === 'user' && m.content[0]?.type === 'text')
.map((m) => {
if (m.content[0]?.type === 'text') {
return m.content[0]?.text?.value
}
return undefined
})
.find((text) => text !== undefined)
try {
const { error } = await supabase.from('threads').insert({
thread_id: thread.id,
run_id: run.id,
user_id: userID,
thread_title: threadTitle as string,
})
if (error) throw error
} catch (error) {
console.error(error)
}
revalidatePath('/profile')
return Response.json({ threadId: thread.id, runId: run.id })
}
@@ -1,9 +1,9 @@
'use client'
import { useEffect, createRef } from 'react'
import { deleteThread } from '@/app/actions'
import { createRef, useEffect } from 'react'
import { useFormState, useFormStatus } from 'react-dom'
import { Button, Input_Shadcn_, Modal } from 'ui'
import { deleteThread } from '@/app/actions'
import { ThreadType } from './Threads'
const ConfirmDeleteThreadModal = ({
@@ -53,8 +53,6 @@ const ConfirmDeleteThreadModal = ({
)
}
console.log('thread', thread)
return (
<Modal
size="small"
@@ -64,12 +62,12 @@ const ConfirmDeleteThreadModal = ({
header="Confirm to delete thread?"
className="pb-2"
>
<form action={formAction} key={`${thread.id}-delete-thread-form`}>
<form action={formAction} key={`${thread.thread_id}-delete-thread-form`}>
<Modal.Content className="py-4">
<p className="text-sm">Once the thread is deleted, it cannot be recovered.</p>
</Modal.Content>
<Modal.Separator />
<Input_Shadcn_ name="thread_id" required type="hidden" value={thread.thread_id} />
<Input_Shadcn_ name="thread_id" value={state.data.thread_id} type="hidden" />
<Modal.Content className="flex flex-row gap-3 justify-end">
<Button type="default">Cancel</Button>
<SubmitButton />
@@ -21,7 +21,7 @@ const EditThreadModal = ({
message: undefined,
success: undefined,
data: {
row_id: thread.id,
thread_id: thread.thread_id,
thread_title: thread.thread_title,
},
}
@@ -56,7 +56,7 @@ const EditThreadModal = ({
header="Edit thread name"
className="pb-2"
>
<form ref={formRef} action={formAction} key={`${thread.id}-update-thread-title-form`}>
<form ref={formRef} action={formAction} key={`${thread.thread_id}-update-thread-title-form`}>
<Modal.Content className="py-4">
<Label_Shadcn_ htmlFor="thread_title">Provide a name for your thread</Label_Shadcn_>
<Input_Shadcn_
@@ -65,7 +65,7 @@ const EditThreadModal = ({
name="thread_title"
defaultValue={state.data.thread_title}
/>
<Input_Shadcn_ name="row_id" value={state.data.row_id} type="hidden" />
<Input_Shadcn_ name="thread_id" value={state.data.thread_id} type="hidden" />
</Modal.Content>
<Modal.Separator />
<Modal.Content className="flex flex-row gap-3 justify-end">
+7 -6
View File
@@ -17,7 +17,8 @@ import EditThreadModal from './EditThreadModal'
import { ThreadType } from './Threads'
const Thread = ({ thread }: { thread: ThreadType }) => {
const formattedTimeAgo = timeAgo(thread.modified_at)
const { created_at, thread_id, thread_title } = thread
const formattedTimeAgo = timeAgo(created_at!)
const [editOpen, setEditOpen] = useState(false)
const [deleteOpen, setDeleteOpen] = useState(false)
@@ -25,15 +26,15 @@ const Thread = ({ thread }: { thread: ThreadType }) => {
return (
<>
<div
key={thread.id}
key={thread_id}
className="group w-full flex flex-row items-center gap-6 border rounded pl-5 pr-2 transition bg-surface-100 hover:bg-surface-200 h-14"
>
<div className="flex flex-col grow overflow-hidden">
<Link
className="flex text-sm group-hover:underline truncate"
href={`/${thread.thread_id}/${thread.run_id}/${thread.latest_message_id}`}
href={`/${thread.thread_id}/${thread.message_id}`}
>
<span className="truncate">{thread.thread_title}</span>
<span className="truncate">{thread_title}</span>
</Link>
<span className="text-xs text-foreground-lighter font-mono">
Last updated {formattedTimeAgo}
@@ -66,13 +67,13 @@ const Thread = ({ thread }: { thread: ThreadType }) => {
</div>
</div>
<ConfirmDeleteThreadModal
key={`${thread.id}-delete-dialog`}
key={`${thread_id}-delete-dialog`}
thread={thread}
visible={deleteOpen}
onClose={() => setDeleteOpen(false)}
/>
<EditThreadModal
key={`${thread.id}-edit-dialog`}
key={`${thread_id}-edit-dialog`}
thread={thread}
visible={editOpen}
onClose={() => setEditOpen(false)}
+7 -6
View File
@@ -1,10 +1,10 @@
import { createClient } from '@/lib/supabase/server'
import { Database } from '@/types/supabase'
import { cookies } from 'next/headers'
import EmptyState from './EmptyState'
import Thread from './Thread'
import { cookies } from 'next/headers'
import { createClient } from '@/lib/supabase/server'
export type ThreadType = Database['public']['Tables']['threads']['Row']
export type ThreadType = Database['public']['Views']['profile_threads']['Row']
async function Threads() {
const cookieStore = cookies()
@@ -17,17 +17,18 @@ async function Threads() {
if (!user) return <p>Error fetching user details</p>
const { data } = await supabase
.from('threads')
.from('profile_threads')
.select()
.eq('user_id', user.id)
.order('created_at', { ascending: false })
const threads = data ?? []
return (
<div className="flex flex-col gap-y-3">
{threads.length > 0 ? (
threads.sort().map((thread) => <Thread key={`thread-item-${thread.id}`} thread={thread} />)
threads
.sort()
.map((thread) => <Thread key={`thread-item-${thread.thread_id}`} thread={thread} />)
) : (
<EmptyState />
)}
+1 -1
View File
@@ -29,7 +29,7 @@ const Profile = async () => {
</div>
<div className="col-span-4 xl:col-span-3 flex flex-col gap-y-4">
<p>Past conversations</p>
<h2>Past conversations</h2>
<div className="w-full h-px border-t" />
@@ -1,117 +0,0 @@
import { Loader2 } from 'lucide-react'
import React, { ChangeEvent, createRef, useEffect } from 'react'
import { useFormStatus } from 'react-dom'
import { TextArea_Shadcn_, cn } from 'ui'
export interface FormProps extends React.FormHTMLAttributes<HTMLFormElement> {
loading?: boolean
disabled?: boolean
value?: string
onValueChange: (value: ChangeEvent<HTMLTextAreaElement>) => void
message?: string
children?: React.ReactNode
}
const AssistantChatForm = React.forwardRef<HTMLFormElement, FormProps>(
({ loading, disabled, value, onValueChange, message, ...props }, ref) => {
const textAreaRef = createRef<HTMLTextAreaElement>()
const submitRef = createRef<HTMLButtonElement>()
useEffect(() => {
if (textAreaRef) {
if (!value && textAreaRef && textAreaRef.current) {
textAreaRef.current.style.height = '40px'
} else if (textAreaRef && textAreaRef.current) {
const newHeight = textAreaRef.current.scrollHeight + 'px'
textAreaRef.current.style.height = newHeight
}
}
}, [value, textAreaRef])
useEffect(() => {
textAreaRef?.current?.focus()
}, [value, textAreaRef])
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
// Check if the pressed key is "Enter" (key code 13) without the "Shift" key
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
if (submitRef.current) {
submitRef.current.click()
}
}
}
const SubmitButton = () => {
const { pending } = useFormStatus()
return (
<div className="absolute right-1.5 top-1.5 flex gap-3 items-center">
{loading || pending ? (
<Loader2 size={22} className="animate-spin w-7 h-7 text-muted" strokeWidth={1} />
) : null}
<button
title="Send AI prompt"
ref={submitRef}
type="submit"
disabled={pending}
className={cn(
'transition-all',
'flex items-center justify-center w-7 h-7 border border-control rounded-full mr-0.5 p-1.5 background-alternative',
!value ? 'text-muted opacity-50' : 'text-default opacity-100',
loading || pending ? 'hidden' : ''
)}
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.5 3V2.25H15V3V10C15 10.5523 14.5522 11 14 11H3.56062L5.53029 12.9697L6.06062 13.5L4.99996 14.5607L4.46963 14.0303L1.39641 10.9571C1.00588 10.5666 1.00588 9.93342 1.39641 9.54289L4.46963 6.46967L4.99996 5.93934L6.06062 7L5.53029 7.53033L3.56062 9.5H13.5V3Z"
fill="currentColor"
></path>
</svg>
</button>
</div>
)
}
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_
name="value"
ref={textAreaRef}
autoFocus
rows={1}
disabled={disabled || submitRef.current?.disabled}
contentEditable
required
className={
'transition-all text-sm pl-12 pr-10 rounded-[18px] resize-none box-border leading-6'
}
placeholder={props.placeholder}
spellCheck={false}
value={value}
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => onValueChange(event)}
onKeyDown={handleKeyDown}
/>
{props.children}
<SubmitButton />
<p aria-live="polite" className="sr-only" role="status">
{message}
</p>
</form>
)
}
)
AssistantChatForm.displayName = 'AssistantChatForm'
export { AssistantChatForm }
@@ -0,0 +1,37 @@
import { useState, useEffect } from 'react'
// something a little funny here maybe?
const textArray = [
"We're working hard to compute your results",
'Installing a new flux capacitor',
'Reconfiguring the warp drive',
'Analyzing the space modulator',
'etc....',
]
// this is just an idea.
// maybe a bad one
// needs better animation if we're keeping it
const ChatLoadingAnimation = () => {
const [currentTextIndex, setCurrentTextIndex] = useState(0)
const [currentText, setCurrentText] = useState(textArray[0])
useEffect(() => {
const interval = setInterval(() => {
setCurrentTextIndex((prevIndex) => (prevIndex + 1) % textArray.length)
setCurrentText(textArray[currentTextIndex])
}, 2000)
return () => {
clearInterval(interval)
}
}, [currentTextIndex, textArray])
return (
<div>
<h1>{currentText}</h1>
</div>
)
}
export default ChatLoadingAnimation
@@ -0,0 +1,31 @@
import { CHAT_EXAMPLES } from '@/data/chat-examples'
import { cn } from '@ui/lib/utils/cn'
import { ExternalLink } from 'lucide-react'
export const ChatSuggestions = ({ setInput }: { setInput: (s: string) => void }) => {
const suggestions = CHAT_EXAMPLES
return (
<div className="flex gap-3 mt-4">
{suggestions.map((suggestion, idx) => (
<button
key={idx}
type="button"
className={cn(
'text-xs',
'flex items-center gap-3 !pr-3',
'transition border rounded-full px-3 py-1.5',
'text-light',
'hover:border-stronger hover:text'
)}
onClick={(event) => {
setInput(suggestion.prompt)
event.preventDefault()
}}
>
{suggestion.label}
<ExternalLink size={12} />
</button>
))}
</div>
)
}
@@ -0,0 +1,10 @@
'use server'
import { upsertThreadMessage } from '@/app/actions'
export async function upsertMessageFormAction(formData: FormData) {
const threadId = formData.get('threadId') as string | undefined
const prompt = formData.get('prompt') as string
return upsertThreadMessage(prompt, threadId)
}
@@ -0,0 +1,148 @@
'use client'
import { Loader2 } from 'lucide-react'
import { useParams } from 'next/navigation'
import { ChangeEvent, FormHTMLAttributes, ReactNode, forwardRef, useRef } from 'react'
import { useFormStatus } from 'react-dom'
import { TextArea_Shadcn_, cn } from 'ui'
import { ChatSuggestions } from './ChatSuggestions'
import { upsertMessageFormAction } from './action'
export interface FormProps extends FormHTMLAttributes<HTMLFormElement> {
loading?: boolean
disabled?: boolean
message?: string
children?: ReactNode
chatContext: 'new' | 'edit'
}
const SubmitButton = forwardRef<HTMLButtonElement, { isLoading: boolean; canSubmit: boolean }>(
({ canSubmit }, ref) => {
const { pending } = useFormStatus()
return (
<div className="absolute right-1.5 top-1.5 flex gap-3 items-center">
{pending && (
<Loader2 size={22} className="animate-spin w-7 h-7 text-muted" strokeWidth={1} />
)}
<button
title="Send AI prompt"
ref={ref}
type="submit"
disabled={pending || canSubmit}
className={cn(
'transition-all',
'flex items-center justify-center w-7 h-7 border border-control rounded-full mr-0.5 p-1.5 background-alternative',
canSubmit ? 'text-default opacity-100' : 'text-muted opacity-50',
pending ? 'hidden' : ''
)}
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.5 3V2.25H15V3V10C15 10.5523 14.5522 11 14 11H3.56062L5.53029 12.9697L6.06062 13.5L4.99996 14.5607L4.46963 14.0303L1.39641 10.9571C1.00588 10.5666 1.00588 9.93342 1.39641 9.54289L4.46963 6.46967L4.99996 5.93934L6.06062 7L5.53029 7.53033L3.56062 9.5H13.5V3Z"
fill="currentColor"
></path>
</svg>
</button>
</div>
)
}
)
SubmitButton.displayName = 'SubmitButton'
interface AssistantChatFormProps {
chatContext: string
placeholder: string
canSubmit?: () => Promise<boolean>
}
const AssistantChatForm = ({
chatContext,
placeholder,
canSubmit = () => Promise.resolve(true),
}: AssistantChatFormProps) => {
const { thread_id } = useParams()
const textAreaRef = useRef<HTMLTextAreaElement>(null)
const disabled = false
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (!e.shiftKey && (e.key === 'Enter' || e.key === 'NumpadEnter')) {
e.preventDefault()
e.currentTarget.form?.requestSubmit()
}
}
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
const target = e.target
let newHeight = target.scrollHeight + 'px'
const input = target.value
if (!input) {
newHeight = '40px'
}
if (target.style.height !== newHeight) {
target.style.height = newHeight
}
}
return (
<>
<form
className="relative"
action={async (formData) => {
const flag = await canSubmit()
if (flag) {
upsertMessageFormAction(formData)
}
}}
>
<div className={cn('absolute', 'top-2 left-2', 'ml-1 w-6 h-6 rounded-full bg-dbnew')}></div>
<input hidden name="threadId" defaultValue={thread_id} />
<TextArea_Shadcn_
name="prompt"
ref={textAreaRef}
autoFocus
rows={1}
defaultValue=""
disabled={disabled}
contentEditable
required
className={
'transition-all text-sm pl-12 pr-10 rounded-[18px] resize-none box-border leading-6'
}
style={{ height: '40px' }}
placeholder={placeholder}
spellCheck={false}
onKeyDown={handleKeyDown}
onChange={handleChange}
/>
<SubmitButton
canSubmit={(textAreaRef?.current?.value || '').length > 0}
isLoading={disabled}
/>
</form>
{chatContext === 'new' && (
<ChatSuggestions
setInput={(v) => {
if (textAreaRef?.current) textAreaRef.current.value = v
}}
/>
)}
</>
)
}
AssistantChatForm.displayName = 'AssistantChatForm'
export { AssistantChatForm }
@@ -1,11 +1,12 @@
import { createClient } from '@/lib/supabase/server'
import { Button } from '@ui/components/Button'
import { Github } from 'lucide-react'
import { cookies, headers } from 'next/headers'
import { redirect } from 'next/navigation'
import { Button } from '@ui/components/Button'
import { createClient } from '@/lib/supabase/server'
export default function LoginForm({ searchParams }: { searchParams?: { message?: string } }) {
const signUp = async (formData: FormData) => {
const signUp = async () => {
'use server'
const origin = headers().get('origin')
@@ -1,28 +0,0 @@
'use client'
import { useParams } from 'next/navigation'
// import { useMessagesQuery } from '@/data/messages-query'
// import { UserMessage } from '@/lib/types'
const CurrentThreadName = () => {
// const { threadId, runId, messageId }: { threadId: string; runId: string; messageId: string } =
// useParams()
// const isConversation = threadId !== undefined && runId !== undefined
// const { data } = useMessagesQuery({ threadId, runId, enabled: isConversation })
// const selectedMessage = data?.messages.find((m) => m.id === messageId) as UserMessage
return (
// <div className="hidden xl:block flex items-center gap-x-4">
// {selectedMessage !== undefined && (
// <p title={selectedMessage.text} className="truncate max-w-[700px] border-l text-sm px-4">
// {selectedMessage.text}
// </p>
// )}
// </div>
<></>
)
}
export default CurrentThreadName
+3 -14
View File
@@ -1,18 +1,9 @@
import HeaderActions from './HeaderActions'
import { createClient } from '@/lib/supabase/server'
import { cookies } from 'next/headers'
import CurrentThreadName from './CurrentThreadName'
import Link from 'next/link'
import HeaderActions from './HeaderActions'
import UserDropdown from './UserDropdown'
const Header = async () => {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const {
data: { user },
} = await supabase.auth.getUser()
return (
<nav
role="navigation"
@@ -26,11 +17,9 @@ const Header = async () => {
<span>design</span>
</div>
</Link>
<CurrentThreadName />
</div>
<div className="flex gap-3">
<HeaderActions user={user} />
<HeaderActions />
<UserDropdown />
</div>
</nav>
@@ -1,38 +1,14 @@
'use client'
import { useAppStateSnapshot } from '@/lib/state'
import { createClient } from '@/lib/supabase/client'
import { User } from '@supabase/supabase-js'
import Link from 'next/link'
import { useRouter, useSelectedLayoutSegment } from 'next/navigation'
import { useState } from 'react'
import { useSelectedLayoutSegment } from 'next/navigation'
import { Button } from 'ui'
import AvatarDropdown from './AvatarDropdown'
import NoUserDropdown from './NoUserDropdown'
import SaveSchemaDropdown from './SaveSchemaDropdown'
import ThemeSwitcherButton from './ThemeSwitcher'
import ToggleCodeEditorButton from './ToggleCodeEditorButton'
interface HeaderActionsProps {
user: User | null
}
const HeaderActions = ({ user }: HeaderActionsProps) => {
const supabase = createClient()
const router = useRouter()
const HeaderActions = () => {
const segment = useSelectedLayoutSegment()
const [currentUser, setCurrentUser] = useState<User | null>(user)
async function signout() {
const { error } = await supabase.auth.signOut()
if (error) {
console.log('Error logging out:', error.message)
return
}
router.push('/')
setCurrentUser(null)
}
return (
<div className="flex items-center gap-x-2">
{segment && segment.includes('thread') && (
@@ -46,12 +22,6 @@ const HeaderActions = ({ user }: HeaderActionsProps) => {
<Button type="default" className="hidden xl:block">
<Link href="/">New conversation</Link>
</Button>
{/* {currentUser ? (
<AvatarDropdown currentUser={currentUser} signout={signout} />
) : (
<NoUserDropdown />
)} */}
</div>
)
}
@@ -1,6 +1,6 @@
'use client'
import { getAppStateSnapshot, useAppStateSnapshot } from '@/lib/state'
import { getAppStateSnapshot } from '@/lib/state'
import Image from 'next/image'
import {
Button,
@@ -14,8 +14,6 @@ import {
} from 'ui'
const SaveSchemaDropdown = () => {
const snap = useAppStateSnapshot()
const copyToClipboard = () => {
const snap = getAppStateSnapshot()
const focused = window.document.hasFocus()
@@ -1,8 +1,6 @@
import { Loader2 } from 'lucide-react'
// output numbers from 1-6 in a span tag
// const numbers = Array.from({ length: 6 }, (_, i) => i + 1)
const GraphLoader = async () => {
const GraphLoader = () => {
return (
<div className="h-full w-full text-muted justify-start border-l">
<div className="flex gap-4">
@@ -1,6 +1,6 @@
import { Loader2 } from 'lucide-react'
const SchemaLoader = async () => {
const SchemaLoader = () => {
return (
<div className="h-full w-full text-muted justify-start">
<div className="flex items-center m-4 gap-2">
@@ -131,7 +131,7 @@ const getLayoutedElements = (nodes: Node[], edges: Edge[]) => {
const dagreGraph = new dagre.graphlib.Graph()
dagreGraph.setDefaultEdgeLabel(() => ({}))
dagreGraph.setGraph({
rankdir: 'LR',
rankdir: 'TB',
align: 'UR',
nodesep: 25,
ranksep: 50,
@@ -1,6 +1,5 @@
'use client'
import { PostgresTable } from '@/lib/types'
import { useTheme } from 'next-themes'
import { useEffect, useMemo, useState } from 'react'
import ReactFlow, {
@@ -10,18 +9,15 @@ import ReactFlow, {
useReactFlow,
} from 'reactflow'
import 'reactflow/dist/style.css'
import { cn } from 'ui'
import { PostgresTable } from '@/lib/types'
import { parseTables } from '@/lib/utils'
import { getGraphDataFromTables } from './SchemaFlow.utils'
import TableNode from './TableNode'
import { useAppStateSnapshot } from '@/lib/state'
import { useParams } from 'next/navigation'
import { set } from 'lodash'
import { parseTables } from '@/lib/utils'
interface SchemaGraphProps {
tables?: PostgresTable[]
content: any
content: string
}
const SchemaFlowHandler = ({ content }: SchemaGraphProps) => {
@@ -39,19 +35,18 @@ const SchemaFlowHandler = ({ content }: SchemaGraphProps) => {
const tables = await parseTables(content)
setTables(tables)
} catch (error) {
// Handle errors
console.log('error', error)
}
}
parseTableData() // Call the async function
parseTableData()
}, [])
useEffect(() => {
getGraphDataFromTables(tables).then(({ nodes, edges }) => {
reactFlowInstance.setNodes(nodes)
reactFlowInstance.setEdges(edges)
setTimeout(() => reactFlowInstance.fitView({}), 10)
setTimeout(() => reactFlowInstance.fitView({ minZoom: 1 }), 50)
})
}, [tables, resolvedTheme, reactFlowInstance])
@@ -93,21 +88,6 @@ const SchemaFlowHandler = ({ content }: SchemaGraphProps) => {
}
const ExportedSchemaGraph = ({ content }: SchemaGraphProps) => {
const snap = useAppStateSnapshot()
const params = useParams()
const runId = params.runId as string
useEffect(() => {
const runIsLoading = snap.runsLoading.includes(runId)
if (runIsLoading) {
// let currentRunsLoading = snap.runsLoading
const payload = [...snap.runsLoading.filter((item) => item !== runId)]
snap.setRunsLoading([...payload])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [runId]) // Intentionally left snap out of the dependency array
return (
<ReactFlowProvider>
<SchemaFlowHandler content={content} />
+1 -1
View File
@@ -1,6 +1,6 @@
const ANIMATION_DELAY = 150
const ShimmeringLoader = ({ className = '', delayIndex = 0, animationDelay = 150 }) => {
const ShimmeringLoader = ({ className = '', delayIndex = 0, animationDelay = ANIMATION_DELAY }) => {
return (
<div
className={`shimmering-loader rounded py-3 ${className}`}
@@ -1,52 +0,0 @@
import { useQuery } from '@tanstack/react-query'
export type ConversationsVariables = {
userId: string
enabled?: boolean
}
// Just update based on the schema - or use the dashboard's type generation feature 😉
export type Conversation = {
id: string
name: string
threadId: string
runId: string
createdAt: string
updatedAt: string
}
export const useConversationsQuery = ({ userId, enabled }: ConversationsVariables) =>
useQuery<Conversation[]>({
queryKey: [userId, 'conversations'],
enabled: enabled && !!userId,
queryFn: async () => {
// [Joshen] Just mocking the conversations data, to replace with fetching from Supabase
const getConversations = (): Promise<Conversation[]> => {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve([
{
id: '1',
name: 'Twitter clone',
threadId: 'thread_vrXIl16bUusqFMUFYDw8CoEy',
runId: 'run_4DiPb5ppb5hwY1y6dT1qEWzc',
createdAt: '2022-07-29 07:53:58.560926+00',
updatedAt: '2022-07-29 07:53:58.560926+00',
},
{
id: '2',
name: 'Supabase clone',
threadId: 'thread_vrXIl16bUusqFMUFYDw8CoEy',
runId: 'run_4DiPb5ppb5hwY1y6dT1qEWzc',
createdAt: '2022-07-29 07:53:58.560926+00',
updatedAt: '2022-07-30 07:53:58.560926+00',
},
])
}, 100)
})
}
const result = await getConversations()
return result
},
})
-28
View File
@@ -1,28 +0,0 @@
// import { useQuery } from '@tanstack/react-query'
// import { ReadThreadAPIResult } from '@/lib/types'
// export type MessagesVariables = {
// threadId: string
// runId: string
// enabled?: boolean
// }
// export const useMessagesQuery = ({ threadId, runId, enabled }: MessagesVariables) =>
// useQuery<ReadThreadAPIResult>({
// queryKey: [threadId, runId],
// enabled: enabled && !!(threadId && runId),
// queryFn: async () => {
// const response = await fetch(`/api/ai/sql/threads/${threadId}/read/${runId}`, {
// method: 'GET',
// })
// const result = await response.json()
// return result
// },
// refetchInterval: (options) => {
// const data = options.state.data
// if (data && data.status === 'completed') {
// return Infinity
// } else return 5000
// },
// })
+3 -1
View File
@@ -6,7 +6,8 @@
"dev": "next dev --port 3002",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@dagrejs/dagre": "^1.0.4",
@@ -14,6 +15,7 @@
"@monaco-editor/react": "^4.6.0",
"@supabase/ssr": "^0.0.10",
"@tanstack/react-query": "^5.7.2",
"ai": "^2.2.31",
"common": "*",
"config": "*",
"dayjs": "^1.11.10",
@@ -1,11 +1,37 @@
create table if not exists public.threads (
id bigint generated by default as identity,
id uuid default gen_random_uuid(),
created_at timestamp with time zone not null default now(),
modified_at timestamp with time zone not null default now(),
thread_id text not null,
run_id text not null,
thread_title text not null,
user_id uuid null,
thread_title text not null,
constraint threads_pkey primary key (id),
constraint threads_user_id_fkey foreign key (user_id) references auth.users (id) on update cascade on delete cascade
)
);
create table if not exists public.messages (
id uuid default gen_random_uuid(),
created_at timestamp with time zone not null default now(),
thread_id uuid not null default gen_random_uuid(),
message_id uuid not null default gen_random_uuid(),
message_role text not null,
message_input text not null,
message_content text not null,
user_id uuid null,
constraint messages_pkey primary key (id),
constraint messages_thread_id_fkey foreign key (thread_id) references threads (id) on update cascade on delete cascade,
constraint messages_user_id_fkey foreign key (user_id) references auth.users (id) on update cascade on delete cascade
);
CREATE
OR REPLACE VIEW profile_threads AS
SELECT
DISTINCT ON (m.thread_id) m.thread_id,
m.message_id,
m.user_id,
m.created_at,
t.thread_title
FROM
public.messages m
JOIN public.threads t ON m.thread_id = t.id
ORDER BY
m.thread_id,
m.created_at ASC;
@@ -1,85 +0,0 @@
alter table "public"."threads" drop constraint "threads_pkey";
drop index if exists "public"."threads_pkey";
create table "public"."messages_user" (
"id" bigint generated by default as identity not null,
"created_at" timestamp with time zone not null default now(),
"modified_at" timestamp with time zone,
"message_id" text,
"thread_id" text,
"text" text,
"run_id" text not null,
"user_id" uuid
);
alter table "public"."messages_user" enable row level security;
create table "public"."responses_ai" (
"id" bigint generated by default as identity not null,
"created_at" timestamp with time zone not null default now(),
"modified_at" timestamp with time zone default now(),
"run_id" text,
"thread_id" text,
"text" text,
"user_id" uuid,
"message_id" text not null
);
alter table "public"."responses_ai" enable row level security;
CREATE UNIQUE INDEX messages_user_pkey ON public.messages_user USING btree (id);
CREATE UNIQUE INDEX responses_message_id_key ON public.responses_ai USING btree (message_id);
CREATE UNIQUE INDEX responses_pkey ON public.responses_ai USING btree (id);
CREATE UNIQUE INDEX threads_thread_id_key ON public.threads USING btree (thread_id);
CREATE UNIQUE INDEX threads_pkey ON public.threads USING btree (id, thread_id);
alter table "public"."messages_user" add constraint "messages_user_pkey" PRIMARY KEY using index "messages_user_pkey";
alter table "public"."responses_ai" add constraint "responses_pkey" PRIMARY KEY using index "responses_pkey";
alter table "public"."threads" add constraint "threads_pkey" PRIMARY KEY using index "threads_pkey";
alter table "public"."messages_user" add constraint "messages_user_thread_id_fkey" FOREIGN KEY (thread_id) REFERENCES threads(thread_id) ON DELETE CASCADE not valid;
alter table "public"."messages_user" validate constraint "messages_user_thread_id_fkey";
alter table "public"."messages_user" add constraint "messages_user_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid;
alter table "public"."messages_user" validate constraint "messages_user_user_id_fkey";
alter table "public"."responses_ai" add constraint "responses_ai_thread_id_fkey" FOREIGN KEY (thread_id) REFERENCES threads(thread_id) not valid;
alter table "public"."responses_ai" validate constraint "responses_ai_thread_id_fkey";
alter table "public"."responses_ai" add constraint "responses_ai_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid;
alter table "public"."responses_ai" validate constraint "responses_ai_user_id_fkey";
alter table "public"."responses_ai" add constraint "responses_message_id_key" UNIQUE using index "responses_message_id_key";
alter table "public"."threads" add constraint "threads_thread_id_key" UNIQUE using index "threads_thread_id_key";
create policy "Enable insert for users based on user_id"
on "public"."messages_user"
as permissive
for insert
to authenticated
with check ((auth.uid() = user_id));
create policy "Enable insert for users based on user_id"
on "public"."responses_ai"
as permissive
for insert
to authenticated
with check ((auth.uid() = user_id));
@@ -1,3 +0,0 @@
alter table "public"."threads" add column "latest_message_id" text;
+126 -83
View File
@@ -3,93 +3,49 @@ export type Json = string | number | boolean | null | { [key: string]: Json | un
export interface Database {
public: {
Tables: {
messages_user: {
messages: {
Row: {
created_at: string
id: number
message_id: string | null
modified_at: string | null
run_id: string
text: string | null
thread_id: string | null
id: string
message_content: string
message_id: string
message_input: string
message_role: string
thread_id: string
user_id: string | null
}
Insert: {
created_at?: string
id?: number
message_id?: string | null
modified_at?: string | null
run_id: string
text?: string | null
thread_id?: string | null
id?: string
message_content: string
message_id?: string
message_input: string
message_role: string
thread_id?: string
user_id?: string | null
}
Update: {
created_at?: string
id?: number
message_id?: string | null
modified_at?: string | null
run_id?: string
text?: string | null
thread_id?: string | null
id?: string
message_content?: string
message_id?: string
message_input?: string
message_role?: string
thread_id?: string
user_id?: string | null
}
Relationships: [
{
foreignKeyName: 'messages_user_thread_id_fkey'
foreignKeyName: 'messages_thread_id_fkey'
columns: ['thread_id']
isOneToOne: false
referencedRelation: 'threads'
referencedColumns: ['thread_id']
},
{
foreignKeyName: 'messages_user_user_id_fkey'
columns: ['user_id']
referencedRelation: 'users'
referencedColumns: ['id']
},
]
}
responses_ai: {
Row: {
created_at: string
id: number
message_id: string
modified_at: string | null
run_id: string | null
text: string | null
thread_id: string | null
user_id: string | null
}
Insert: {
created_at?: string
id?: number
message_id: string
modified_at?: string | null
run_id?: string | null
text?: string | null
thread_id?: string | null
user_id?: string | null
}
Update: {
created_at?: string
id?: number
message_id?: string
modified_at?: string | null
run_id?: string | null
text?: string | null
thread_id?: string | null
user_id?: string | null
}
Relationships: [
{
foreignKeyName: 'responses_ai_thread_id_fkey'
columns: ['thread_id']
referencedRelation: 'threads'
referencedColumns: ['thread_id']
},
{
foreignKeyName: 'responses_ai_user_id_fkey'
foreignKeyName: 'messages_user_id_fkey'
columns: ['user_id']
isOneToOne: false
referencedRelation: 'users'
referencedColumns: ['id']
},
@@ -98,31 +54,19 @@ export interface Database {
threads: {
Row: {
created_at: string
id: number
latest_message_id: string | null
modified_at: string
run_id: string
thread_id: string
id: string
thread_title: string
user_id: string | null
}
Insert: {
created_at?: string
id?: number
latest_message_id?: string | null
modified_at?: string
run_id: string
thread_id: string
id?: string
thread_title: string
user_id?: string | null
}
Update: {
created_at?: string
id?: number
latest_message_id?: string | null
modified_at?: string
run_id?: string
thread_id?: string
id?: string
thread_title?: string
user_id?: string | null
}
@@ -130,6 +74,7 @@ export interface Database {
{
foreignKeyName: 'threads_user_id_fkey'
columns: ['user_id']
isOneToOne: false
referencedRelation: 'users'
referencedColumns: ['id']
},
@@ -137,7 +82,31 @@ export interface Database {
}
}
Views: {
[_ in never]: never
profile_threads: {
Row: {
created_at: string | null
message_id: string | null
thread_id: string | null
thread_title: string | null
user_id: string | null
}
Relationships: [
{
foreignKeyName: 'messages_thread_id_fkey'
columns: ['thread_id']
isOneToOne: false
referencedRelation: 'threads'
referencedColumns: ['id']
},
{
foreignKeyName: 'messages_user_id_fkey'
columns: ['user_id']
isOneToOne: false
referencedRelation: 'users'
referencedColumns: ['id']
},
]
}
}
Functions: {
[_ in never]: never
@@ -150,3 +119,77 @@ export interface Database {
}
}
}
export type Tables<
PublicTableNameOrOptions extends
| keyof (Database['public']['Tables'] & Database['public']['Views'])
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof (Database[PublicTableNameOrOptions['schema']]['Tables'] &
Database[PublicTableNameOrOptions['schema']]['Views'])
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? (Database[PublicTableNameOrOptions['schema']]['Tables'] &
Database[PublicTableNameOrOptions['schema']]['Views'])[TableName] extends {
Row: infer R
}
? R
: never
: PublicTableNameOrOptions extends keyof (Database['public']['Tables'] &
Database['public']['Views'])
? (Database['public']['Tables'] &
Database['public']['Views'])[PublicTableNameOrOptions] extends {
Row: infer R
}
? R
: never
: never
export type TablesInsert<
PublicTableNameOrOptions extends keyof Database['public']['Tables'] | { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions['schema']]['Tables']
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends {
Insert: infer I
}
? I
: never
: PublicTableNameOrOptions extends keyof Database['public']['Tables']
? Database['public']['Tables'][PublicTableNameOrOptions] extends {
Insert: infer I
}
? I
: never
: never
export type TablesUpdate<
PublicTableNameOrOptions extends keyof Database['public']['Tables'] | { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions['schema']]['Tables']
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends {
Update: infer U
}
? U
: never
: PublicTableNameOrOptions extends keyof Database['public']['Tables']
? Database['public']['Tables'][PublicTableNameOrOptions] extends {
Update: infer U
}
? U
: never
: never
export type Enums<
PublicEnumNameOrOptions extends keyof Database['public']['Enums'] | { schema: keyof Database },
EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicEnumNameOrOptions['schema']]['Enums']
: never = never,
> = PublicEnumNameOrOptions extends { schema: keyof Database }
? Database[PublicEnumNameOrOptions['schema']]['Enums'][EnumName]
: PublicEnumNameOrOptions extends keyof Database['public']['Enums']
? Database['public']['Enums'][PublicEnumNameOrOptions]
: never
@@ -11,7 +11,7 @@ import { AIPolicyPre } from './AIPolicyPre'
interface MessageProps {
name?: string
role: 'function' | 'user' | 'assistant' | 'system' | 'data'
role: 'function' | 'system' | 'user' | 'assistant' | 'data' | 'tool'
content?: string
createdAt?: number
isDebug?: boolean
+4 -3
View File
@@ -38,6 +38,7 @@
"@monaco-editor/react": "^4.6.0",
"@supabase/ssr": "^0.0.10",
"@tanstack/react-query": "^5.7.2",
"ai": "^2.2.31",
"common": "*",
"config": "*",
"dayjs": "^1.11.10",
@@ -12985,9 +12986,9 @@
}
},
"node_modules/ai": {
"version": "2.2.29",
"resolved": "https://registry.npmjs.org/ai/-/ai-2.2.29.tgz",
"integrity": "sha512-/zzSTTKF5LxMGQuNVUnNjs7X6PWYfb6M88Zn74gCUnM3KCYgh0CiAWhLyhKP6UtK0H5mHSmXgt0ZkZYUecRp0w==",
"version": "2.2.31",
"resolved": "https://registry.npmjs.org/ai/-/ai-2.2.31.tgz",
"integrity": "sha512-WQH13RxP+RYo9IE/FX8foNQh9gcKO/dhl9OGy5JL2bHJVBlnugPmH2CYJWaRt+mvjXHaU8txB+jzGo/fbtH2HA==",
"dependencies": {
"eventsource-parser": "1.0.0",
"nanoid": "3.3.6",
-210
View File
@@ -1,210 +0,0 @@
import { DataGridHandle } from 'react-data-grid'
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { createPortal } from 'react-dom'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
import { useUrlState } from 'hooks'
import {
cleanupProps,
formatFilterURLParams,
formatSortURLParams,
initTable,
saveStorageDebounced,
} from './SupabaseGrid.utils'
import { Shortcuts } from './components/common'
import Footer from './components/footer'
import { Grid } from './components/grid'
import Header from './components/header'
import { RowContextMenu } from './components/menu'
import { StoreProvider, useDispatch, useTrackedState } from './store'
import { Dictionary, SupabaseGridProps, SupabaseGridRef } from './types'
/** Supabase Grid: React component to render database table */
export const SupabaseGrid = forwardRef<SupabaseGridRef, SupabaseGridProps>((props, ref) => {
const _props = cleanupProps(props)
return (
<StoreProvider>
<DndProvider backend={HTML5Backend}>
<SupabaseGridLayout ref={ref} {..._props} />
</DndProvider>
</StoreProvider>
)
})
const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>(
function SupabaseGridLayout(props, ref) {
const {
editable,
storageRef,
gridProps,
headerActions,
showCustomChildren,
customHeader,
children,
onAddRow,
onAddColumn,
updateTableRow,
onEditForeignKeyColumnValue,
onImportData,
} = props
const dispatch = useDispatch()
const state = useTrackedState()
const gridRef = useRef<DataGridHandle>(null)
const [mounted, setMounted] = useState(false)
const [{ sort, filter }, setParams] = useUrlState({
arrayKeys: ['sort', 'filter'],
})
const sorts = formatSortURLParams(sort as string[])
const filters = formatFilterURLParams(filter as string[])
const { project } = useProjectContext()
const { data, error, isSuccess, isError, isLoading, isRefetching } = useTableRowsQuery(
{
queryKey: [props.table.schema, props.table.name],
projectRef: project?.ref,
connectionString: project?.connectionString,
table: props.table,
sorts,
filters,
page: state.page,
limit: state.rowsPerPage,
},
{
keepPreviousData: true,
onSuccess(data) {
dispatch({
type: 'SET_ROWS_COUNT',
payload: data.rows.length,
})
},
}
)
useImperativeHandle(ref, () => ({
rowAdded(row: Dictionary<any>) {
dispatch({
type: 'ADD_NEW_ROW',
payload: row,
})
},
rowEdited(row: Dictionary<any>, idx: number) {
dispatch({
type: 'EDIT_ROW',
payload: { row, idx },
})
},
}))
useEffect(() => {
if (!mounted) setMounted(true)
}, [])
useEffect(() => {
if (mounted) {
dispatch({ type: 'UPDATE_FILTERS', payload: {} })
}
}, [JSON.stringify(filters)])
useEffect(() => {
if (mounted) {
dispatch({ type: 'UPDATE_SORTS', payload: {} })
}
}, [JSON.stringify(sorts)])
useEffect(() => {
if (state.isInitialComplete && storageRef && state.table) {
saveStorageDebounced(state, storageRef, sort as string[], filter as string[])
}
}, [
state.table,
state.isInitialComplete,
state.gridColumns,
JSON.stringify(sorts),
JSON.stringify(filters),
storageRef,
])
useEffect(() => {
dispatch({
type: 'INIT_CALLBACK',
payload: { ...props },
})
}, [])
useEffect(() => {
const initializeData = async () => {
const { savedState } = await initTable(
props,
state,
dispatch,
sort as string[],
filter as string[]
)
if (savedState.sorts || savedState.filters) {
setParams((prevParams) => {
return {
...prevParams,
...(savedState.sorts && { sort: savedState.sorts }),
...(savedState.filters && { filter: savedState.filters }),
}
})
}
}
const refreshTable = JSON.stringify(props.table) !== JSON.stringify(state.table)
if (!state.table || refreshTable) {
initializeData()
}
}, [state.table, props.table, props.schema])
return (
<div className="sb-grid">
<Header
table={props.table}
sorts={sorts}
filters={filters}
isRefetching={isRefetching}
onAddRow={editable ? onAddRow : undefined}
onAddColumn={editable ? onAddColumn : undefined}
onImportData={editable ? onImportData : undefined}
headerActions={headerActions}
customHeader={customHeader}
/>
{showCustomChildren && children !== undefined ? (
<>{children}</>
) : (
<>
<Grid
ref={gridRef}
{...gridProps}
rows={data?.rows ?? []}
error={error}
isLoading={isLoading}
isSuccess={isSuccess}
isError={isError}
filters={filters}
setParams={setParams}
updateRow={updateTableRow}
onAddRow={onAddRow}
onImportData={onImportData}
onEditForeignKeyColumnValue={onEditForeignKeyColumnValue}
/>
</>
)}
<Shortcuts gridRef={gridRef} />
<Footer isLoading={isLoading || isRefetching} isRefetching={isRefetching} />
{mounted && createPortal(<RowContextMenu rows={data?.rows ?? []} />, document.body)}
</div>
)
}
)