mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
1 parent
02e7e9cf87
commit
b4fa734c85
58 files changed
+899
-1689
No files matched your search
@@ -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
|
||||
File renamed without changes.
@@ -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>
|
||||
)
|
||||
}
|
||||
File renamed without changes.
+15
-49
@@ -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
@@ -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">
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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 />
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
@@ -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">
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -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,
|
||||
File renamed without changes.
+5
-25
@@ -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,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
|
||||
},
|
||||
})
|
||||
@@ -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
|
||||
// },
|
||||
// })
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+4
-3
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
)
|
||||
Reference in new issue
Block a user