From b4fa734c857b4abdc5bd1ef9f635edad09e0b0b9 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Mon, 19 Feb 2024 15:52:58 -0330 Subject: [PATCH] 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 --- apps/database-new/app/NewThreadInput.tsx | 81 +--- .../app/[threadId]/[runId]/Chat.tsx | 36 -- .../app/[threadId]/[runId]/ChatInput.tsx | 63 ---- .../app/[threadId]/[runId]/Messages.tsx | 56 --- .../[runId]/[messageId]/CodeEditor.tsx | 50 --- .../[runId]/[messageId]/MessageId.utils.ts | 66 ---- .../[runId]/[messageId]/SchemaFlow.tsx | 97 ----- .../[threadId]/[runId]/[messageId]/page.tsx | 29 -- .../app/[threadId]/[runId]/layout.tsx | 22 -- .../app/[threadId]/[runId]/page----delete.tsx | 74 ---- .../[message_id]}/BottomMarker.tsx | 0 .../[thread_id]/[message_id]/CodeEditor.tsx | 23 ++ .../[message_id]}/CodeEditorContainer.tsx | 0 .../[message_id]/MessageItem.tsx} | 64 +--- .../app/[thread_id]/[message_id]/Messages.tsx | 62 +++ .../[thread_id]/[message_id]/SchemaFlow.tsx | 8 + .../[message_id]/getAssistantMessage.ts | 135 +++++++ .../[thread_id]/[message_id]/getMessages.ts | 10 + .../app/[thread_id]/[message_id]/page.tsx | 56 +++ apps/database-new/app/actions.ts | 356 ++++++++---------- .../ai/sql/threads/[threadId]/delete/route.ts | 13 - .../threads/[threadId]/read/[runId]/route.ts | 50 --- .../ai/sql/threads/[threadId]/update/route.ts | 22 -- .../app/api/ai/sql/threads/create/route.ts | 58 --- .../app/profile/ConfirmDeleteThreadModal.tsx | 10 +- .../app/profile/EditThreadModal.tsx | 6 +- apps/database-new/app/profile/Thread.tsx | 13 +- apps/database-new/app/profile/Threads.tsx | 13 +- apps/database-new/app/profile/page.tsx | 2 +- .../components/AssistantChatForm.tsx | 117 ------ .../ChatLoadingAnimation.tsx | 37 ++ .../AssistantChatForm/ChatSuggestions.tsx | 31 ++ .../components/AssistantChatForm/action.ts | 10 + .../components/AssistantChatForm/index.tsx | 148 ++++++++ .../components/Auth/LoginForm.tsx | 7 +- .../components/Header/CurrentThreadName.tsx | 28 -- .../database-new/components/Header/Header.tsx | 17 +- .../components/Header/HeaderActions.tsx | 36 +- .../components/Header/SaveSchemaDropdown.tsx | 4 +- .../components/Loaders/GraphLoader.tsx | 4 +- .../components/Loaders/SchemaLoader.tsx | 2 +- .../MonacoEditor}/CodeEditor.utils.ts | 0 .../MonacoEditor/index.tsx} | 0 .../SchemaFlow.constants.ts | 0 .../SchemaFlowHandler}/SchemaFlow.utils.ts | 2 +- .../SchemaFlowHandler}/TableNode.tsx | 0 .../SchemaFlowHandler/index.tsx} | 30 +- apps/database-new/components/Shimmer.tsx | 2 +- apps/database-new/data/conversations-query.ts | 52 --- apps/database-new/data/messages-query.ts | 28 -- apps/database-new/package.json | 4 +- .../migrations/20231122191507_get_started.sql | 38 +- .../20231204135129_remote_schema.sql | 85 ----- .../20231204164321_remote_schema.sql | 3 - apps/database-new/types/supabase.ts | 209 ++++++---- .../Policies/AIPolicyEditorPanel/Message.tsx | 2 +- package-lock.json | 7 +- studio/components/grid/SupabaseGrid.tsx | 210 ----------- 58 files changed, 899 insertions(+), 1689 deletions(-) delete mode 100644 apps/database-new/app/[threadId]/[runId]/Chat.tsx delete mode 100644 apps/database-new/app/[threadId]/[runId]/ChatInput.tsx delete mode 100644 apps/database-new/app/[threadId]/[runId]/Messages.tsx delete mode 100644 apps/database-new/app/[threadId]/[runId]/[messageId]/CodeEditor.tsx delete mode 100644 apps/database-new/app/[threadId]/[runId]/[messageId]/MessageId.utils.ts delete mode 100644 apps/database-new/app/[threadId]/[runId]/[messageId]/SchemaFlow.tsx delete mode 100644 apps/database-new/app/[threadId]/[runId]/[messageId]/page.tsx delete mode 100644 apps/database-new/app/[threadId]/[runId]/layout.tsx delete mode 100644 apps/database-new/app/[threadId]/[runId]/page----delete.tsx rename apps/database-new/app/{[threadId]/[runId] => [thread_id]/[message_id]}/BottomMarker.tsx (100%) create mode 100644 apps/database-new/app/[thread_id]/[message_id]/CodeEditor.tsx rename apps/database-new/app/{[threadId]/[runId]/[messageId] => [thread_id]/[message_id]}/CodeEditorContainer.tsx (100%) rename apps/database-new/app/{[threadId]/[runId]/UserChat.tsx => [thread_id]/[message_id]/MessageItem.tsx} (62%) create mode 100644 apps/database-new/app/[thread_id]/[message_id]/Messages.tsx create mode 100644 apps/database-new/app/[thread_id]/[message_id]/SchemaFlow.tsx create mode 100644 apps/database-new/app/[thread_id]/[message_id]/getAssistantMessage.ts create mode 100644 apps/database-new/app/[thread_id]/[message_id]/getMessages.ts create mode 100644 apps/database-new/app/[thread_id]/[message_id]/page.tsx delete mode 100644 apps/database-new/app/api/ai/sql/threads/[threadId]/delete/route.ts delete mode 100644 apps/database-new/app/api/ai/sql/threads/[threadId]/read/[runId]/route.ts delete mode 100644 apps/database-new/app/api/ai/sql/threads/[threadId]/update/route.ts delete mode 100644 apps/database-new/app/api/ai/sql/threads/create/route.ts delete mode 100644 apps/database-new/components/AssistantChatForm.tsx create mode 100644 apps/database-new/components/AssistantChatForm/ChatLoadingAnimation.tsx create mode 100644 apps/database-new/components/AssistantChatForm/ChatSuggestions.tsx create mode 100644 apps/database-new/components/AssistantChatForm/action.ts create mode 100644 apps/database-new/components/AssistantChatForm/index.tsx delete mode 100644 apps/database-new/components/Header/CurrentThreadName.tsx rename apps/database-new/{app/[threadId]/[runId]/[messageId] => components/MonacoEditor}/CodeEditor.utils.ts (100%) rename apps/database-new/{app/[threadId]/[runId]/[messageId]/MonacoEditor.tsx => components/MonacoEditor/index.tsx} (100%) rename apps/database-new/{app/[threadId]/[runId]/[messageId] => components/SchemaFlowHandler}/SchemaFlow.constants.ts (100%) rename apps/database-new/{app/[threadId]/[runId]/[messageId] => components/SchemaFlowHandler}/SchemaFlow.utils.ts (99%) rename apps/database-new/{app/[threadId]/[runId]/[messageId] => components/SchemaFlowHandler}/TableNode.tsx (100%) rename apps/database-new/{app/[threadId]/[runId]/[messageId]/SchemaFlowHandler.tsx => components/SchemaFlowHandler/index.tsx} (74%) delete mode 100644 apps/database-new/data/conversations-query.ts delete mode 100644 apps/database-new/data/messages-query.ts delete mode 100644 apps/database-new/supabase/migrations/20231204135129_remote_schema.sql delete mode 100644 apps/database-new/supabase/migrations/20231204164321_remote_schema.sql delete mode 100644 studio/components/grid/SupabaseGrid.tsx diff --git a/apps/database-new/app/NewThreadInput.tsx b/apps/database-new/app/NewThreadInput.tsx index 13881583875..e0183639aa0 100644 --- a/apps/database-new/app/NewThreadInput.tsx +++ b/apps/database-new/app/NewThreadInput.tsx @@ -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 = () => { <>
{ + 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 &&

{state?.message}

} -
-
- {suggestions.map((suggestion, idx) => ( - - ))}
) diff --git a/apps/database-new/app/[threadId]/[runId]/Chat.tsx b/apps/database-new/app/[threadId]/[runId]/Chat.tsx deleted file mode 100644 index f4aafefba87..00000000000 --- a/apps/database-new/app/[threadId]/[runId]/Chat.tsx +++ /dev/null @@ -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 ( -
-
- -
- loading

}> - -
- -
-
- -
-
- ) -} - -export { Chat } diff --git a/apps/database-new/app/[threadId]/[runId]/ChatInput.tsx b/apps/database-new/app/[threadId]/[runId]/ChatInput.tsx deleted file mode 100644 index 64bf1af14dd..00000000000 --- a/apps/database-new/app/[threadId]/[runId]/ChatInput.tsx +++ /dev/null @@ -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 ? ( -

{state.message}

- ) : null - } - - return ( -
- setValue(v.target.value)} - message={state.message} - > - - - - -
- ) -} - -export { ChatInput } diff --git a/apps/database-new/app/[threadId]/[runId]/Messages.tsx b/apps/database-new/app/[threadId]/[runId]/Messages.tsx deleted file mode 100644 index c4fb282dd94..00000000000 --- a/apps/database-new/app/[threadId]/[runId]/Messages.tsx +++ /dev/null @@ -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 ( -
- {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 ( - - ) - })} -
- ) -} - -export { Messages } diff --git a/apps/database-new/app/[threadId]/[runId]/[messageId]/CodeEditor.tsx b/apps/database-new/app/[threadId]/[runId]/[messageId]/CodeEditor.tsx deleted file mode 100644 index 272ba15558f..00000000000 --- a/apps/database-new/app/[threadId]/[runId]/[messageId]/CodeEditor.tsx +++ /dev/null @@ -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 ( - - - - ) -} diff --git a/apps/database-new/app/[threadId]/[runId]/[messageId]/MessageId.utils.ts b/apps/database-new/app/[threadId]/[runId]/[messageId]/MessageId.utils.ts deleted file mode 100644 index 73e2ef4ccb1..00000000000 --- a/apps/database-new/app/[threadId]/[runId]/[messageId]/MessageId.utils.ts +++ /dev/null @@ -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 } diff --git a/apps/database-new/app/[threadId]/[runId]/[messageId]/SchemaFlow.tsx b/apps/database-new/app/[threadId]/[runId]/[messageId]/SchemaFlow.tsx deleted file mode 100644 index 8f221fac4ae..00000000000 --- a/apps/database-new/app/[threadId]/[runId]/[messageId]/SchemaFlow.tsx +++ /dev/null @@ -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 -} diff --git a/apps/database-new/app/[threadId]/[runId]/[messageId]/page.tsx b/apps/database-new/app/[threadId]/[runId]/[messageId]/page.tsx deleted file mode 100644 index eca31d6840f..00000000000 --- a/apps/database-new/app/[threadId]/[runId]/[messageId]/page.tsx +++ /dev/null @@ -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 ( -
- }> - - - - }> - - -
- ) -} diff --git a/apps/database-new/app/[threadId]/[runId]/layout.tsx b/apps/database-new/app/[threadId]/[runId]/layout.tsx deleted file mode 100644 index c8bf55dbef4..00000000000 --- a/apps/database-new/app/[threadId]/[runId]/layout.tsx +++ /dev/null @@ -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 ( -
- -
- - -
- {children} -
- ) -} diff --git a/apps/database-new/app/[threadId]/[runId]/page----delete.tsx b/apps/database-new/app/[threadId]/[runId]/page----delete.tsx deleted file mode 100644 index 87b9c8530a3..00000000000 --- a/apps/database-new/app/[threadId]/[runId]/page----delete.tsx +++ /dev/null @@ -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 diff --git a/apps/database-new/app/[threadId]/[runId]/BottomMarker.tsx b/apps/database-new/app/[thread_id]/[message_id]/BottomMarker.tsx similarity index 100% rename from apps/database-new/app/[threadId]/[runId]/BottomMarker.tsx rename to apps/database-new/app/[thread_id]/[message_id]/BottomMarker.tsx diff --git a/apps/database-new/app/[thread_id]/[message_id]/CodeEditor.tsx b/apps/database-new/app/[thread_id]/[message_id]/CodeEditor.tsx new file mode 100644 index 00000000000..8197c3ab6ac --- /dev/null +++ b/apps/database-new/app/[thread_id]/[message_id]/CodeEditor.tsx @@ -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 }) { + 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 ( + + + + ) +} diff --git a/apps/database-new/app/[threadId]/[runId]/[messageId]/CodeEditorContainer.tsx b/apps/database-new/app/[thread_id]/[message_id]/CodeEditorContainer.tsx similarity index 100% rename from apps/database-new/app/[threadId]/[runId]/[messageId]/CodeEditorContainer.tsx rename to apps/database-new/app/[thread_id]/[message_id]/CodeEditorContainer.tsx diff --git a/apps/database-new/app/[threadId]/[runId]/UserChat.tsx b/apps/database-new/app/[thread_id]/[message_id]/MessageItem.tsx similarity index 62% rename from apps/database-new/app/[threadId]/[runId]/UserChat.tsx rename to apps/database-new/app/[thread_id]/[message_id]/MessageItem.tsx index f1457b55ff6..98aa11796d8 100644 --- a/apps/database-new/app/[threadId]/[runId]/UserChat.tsx +++ b/apps/database-new/app/[thread_id]/[message_id]/MessageItem.tsx @@ -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 ( { isSelected && 'bg-surface-200', isSelected ? 'border-r-foreground' : 'border-r border-r-transparent' )} - href={`/${threadId}/${runId}/${message.id}`} + href={`/${thread_id}/${message.id}`} >
{ : 'bg-transparent border-foreground-muted group-hover:border-foreground' )} /> + {isLoading && ( {
)} + {/* Node line*/} {!isLatest &&
}
@@ -106,7 +77,7 @@ const UserChat = ({ message, isLatest, times, run }: UserChatProps) => {
{ isSelected ? 'text-foreground' : 'text-light group-hover:text-foreground' )} > - {text} + {message.content}

- {/* {isLoading &&
} */} + {isLoading &&
}
{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...' - : ''}

)}
@@ -147,4 +113,4 @@ const UserChat = ({ message, isLatest, times, run }: UserChatProps) => { ) } -export default UserChat +export default MessageItem diff --git a/apps/database-new/app/[thread_id]/[message_id]/Messages.tsx b/apps/database-new/app/[thread_id]/[message_id]/Messages.tsx new file mode 100644 index 00000000000..6954b2b1957 --- /dev/null +++ b/apps/database-new/app/[thread_id]/[message_id]/Messages.tsx @@ -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 ( + +
+ {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 ( + + ) + })} +
+ +
+ ) +} diff --git a/apps/database-new/app/[thread_id]/[message_id]/SchemaFlow.tsx b/apps/database-new/app/[thread_id]/[message_id]/SchemaFlow.tsx new file mode 100644 index 00000000000..e53a476920a --- /dev/null +++ b/apps/database-new/app/[thread_id]/[message_id]/SchemaFlow.tsx @@ -0,0 +1,8 @@ +import SchemaFlowHandler from '@/components/SchemaFlowHandler' + +export async function SchemaFlow({ promisedMessage }: { promisedMessage: Promise }) { + const code = await promisedMessage + const strippedCode = code.replace('```sql\n', '').replace('\n```', '') + + return +} diff --git a/apps/database-new/app/[thread_id]/[message_id]/getAssistantMessage.ts b/apps/database-new/app/[thread_id]/[message_id]/getAssistantMessage.ts new file mode 100644 index 00000000000..00793d04506 --- /dev/null +++ b/apps/database-new/app/[thread_id]/[message_id]/getAssistantMessage.ts @@ -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 +} diff --git a/apps/database-new/app/[thread_id]/[message_id]/getMessages.ts b/apps/database-new/app/[thread_id]/[message_id]/getMessages.ts new file mode 100644 index 00000000000..f4ea971894b --- /dev/null +++ b/apps/database-new/app/[thread_id]/[message_id]/getMessages.ts @@ -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') +}) diff --git a/apps/database-new/app/[thread_id]/[message_id]/page.tsx b/apps/database-new/app/[thread_id]/[message_id]/page.tsx new file mode 100644 index 00000000000..4ff81c18eb7 --- /dev/null +++ b/apps/database-new/app/[thread_id]/[message_id]/page.tsx @@ -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 ( +
+
+
+ +
+ +
+
+
+
+ + +
+ +
+ }> + + + }> + + +
+
+ ) +} diff --git a/apps/database-new/app/actions.ts b/apps/database-new/app/actions.ts index 6b5ebebdd93..3d17b972031 100644 --- a/apps/database-new/app/actions.ts +++ b/apps/database-new/app/actions.ts @@ -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) -} diff --git a/apps/database-new/app/api/ai/sql/threads/[threadId]/delete/route.ts b/apps/database-new/app/api/ai/sql/threads/[threadId]/delete/route.ts deleted file mode 100644 index ac5b4e0554f..00000000000 --- a/apps/database-new/app/api/ai/sql/threads/[threadId]/delete/route.ts +++ /dev/null @@ -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({}) -} diff --git a/apps/database-new/app/api/ai/sql/threads/[threadId]/read/[runId]/route.ts b/apps/database-new/app/api/ai/sql/threads/[threadId]/read/[runId]/route.ts deleted file mode 100644 index d15c0087d08..00000000000 --- a/apps/database-new/app/api/ai/sql/threads/[threadId]/read/[runId]/route.ts +++ /dev/null @@ -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) -} diff --git a/apps/database-new/app/api/ai/sql/threads/[threadId]/update/route.ts b/apps/database-new/app/api/ai/sql/threads/[threadId]/update/route.ts deleted file mode 100644 index 010e0fd6b07..00000000000 --- a/apps/database-new/app/api/ai/sql/threads/[threadId]/update/route.ts +++ /dev/null @@ -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 }) -} diff --git a/apps/database-new/app/api/ai/sql/threads/create/route.ts b/apps/database-new/app/api/ai/sql/threads/create/route.ts deleted file mode 100644 index 8f12d5476b9..00000000000 --- a/apps/database-new/app/api/ai/sql/threads/create/route.ts +++ /dev/null @@ -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 }) -} diff --git a/apps/database-new/app/profile/ConfirmDeleteThreadModal.tsx b/apps/database-new/app/profile/ConfirmDeleteThreadModal.tsx index b3bc7c3833d..ddfc7c8e937 100644 --- a/apps/database-new/app/profile/ConfirmDeleteThreadModal.tsx +++ b/apps/database-new/app/profile/ConfirmDeleteThreadModal.tsx @@ -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 ( -
+

Once the thread is deleted, it cannot be recovered.

- + diff --git a/apps/database-new/app/profile/EditThreadModal.tsx b/apps/database-new/app/profile/EditThreadModal.tsx index 88edb787191..8563ea7c14a 100644 --- a/apps/database-new/app/profile/EditThreadModal.tsx +++ b/apps/database-new/app/profile/EditThreadModal.tsx @@ -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" > - + Provide a name for your thread - + diff --git a/apps/database-new/app/profile/Thread.tsx b/apps/database-new/app/profile/Thread.tsx index 22b9a2caa69..d876d11bb78 100644 --- a/apps/database-new/app/profile/Thread.tsx +++ b/apps/database-new/app/profile/Thread.tsx @@ -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 ( <>
- {thread.thread_title} + {thread_title} Last updated {formattedTimeAgo} @@ -66,13 +67,13 @@ const Thread = ({ thread }: { thread: ThreadType }) => {
setDeleteOpen(false)} /> setEditOpen(false)} diff --git a/apps/database-new/app/profile/Threads.tsx b/apps/database-new/app/profile/Threads.tsx index 4405473e556..9cf8fc6b226 100644 --- a/apps/database-new/app/profile/Threads.tsx +++ b/apps/database-new/app/profile/Threads.tsx @@ -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

Error fetching user details

const { data } = await supabase - .from('threads') + .from('profile_threads') .select() .eq('user_id', user.id) .order('created_at', { ascending: false }) - const threads = data ?? [] return (
{threads.length > 0 ? ( - threads.sort().map((thread) => ) + threads + .sort() + .map((thread) => ) ) : ( )} diff --git a/apps/database-new/app/profile/page.tsx b/apps/database-new/app/profile/page.tsx index 0183e144d71..07c0078992d 100644 --- a/apps/database-new/app/profile/page.tsx +++ b/apps/database-new/app/profile/page.tsx @@ -29,7 +29,7 @@ const Profile = async () => {
-

Past conversations

+

Past conversations

diff --git a/apps/database-new/components/AssistantChatForm.tsx b/apps/database-new/components/AssistantChatForm.tsx deleted file mode 100644 index 7844ceaf017..00000000000 --- a/apps/database-new/components/AssistantChatForm.tsx +++ /dev/null @@ -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 { - loading?: boolean - disabled?: boolean - value?: string - onValueChange: (value: ChangeEvent) => void - message?: string - children?: React.ReactNode -} - -const AssistantChatForm = React.forwardRef( - ({ loading, disabled, value, onValueChange, message, ...props }, ref) => { - const textAreaRef = createRef() - const submitRef = createRef() - - 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) => { - // 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 ( -
- {loading || pending ? ( - - ) : null} - - -
- ) - } - - return ( - -
- ) => onValueChange(event)} - onKeyDown={handleKeyDown} - /> - {props.children} - -

- {message} -

- - ) - } -) - -AssistantChatForm.displayName = 'AssistantChatForm' - -export { AssistantChatForm } diff --git a/apps/database-new/components/AssistantChatForm/ChatLoadingAnimation.tsx b/apps/database-new/components/AssistantChatForm/ChatLoadingAnimation.tsx new file mode 100644 index 00000000000..c59f22d5ab3 --- /dev/null +++ b/apps/database-new/components/AssistantChatForm/ChatLoadingAnimation.tsx @@ -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 ( +
+

{currentText}

+
+ ) +} + +export default ChatLoadingAnimation diff --git a/apps/database-new/components/AssistantChatForm/ChatSuggestions.tsx b/apps/database-new/components/AssistantChatForm/ChatSuggestions.tsx new file mode 100644 index 00000000000..d38d09f5fbe --- /dev/null +++ b/apps/database-new/components/AssistantChatForm/ChatSuggestions.tsx @@ -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 ( +
+ {suggestions.map((suggestion, idx) => ( + + ))} +
+ ) +} diff --git a/apps/database-new/components/AssistantChatForm/action.ts b/apps/database-new/components/AssistantChatForm/action.ts new file mode 100644 index 00000000000..546532e2e45 --- /dev/null +++ b/apps/database-new/components/AssistantChatForm/action.ts @@ -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) +} diff --git a/apps/database-new/components/AssistantChatForm/index.tsx b/apps/database-new/components/AssistantChatForm/index.tsx new file mode 100644 index 00000000000..df77176b4cd --- /dev/null +++ b/apps/database-new/components/AssistantChatForm/index.tsx @@ -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 { + loading?: boolean + disabled?: boolean + message?: string + children?: ReactNode + chatContext: 'new' | 'edit' +} + +const SubmitButton = forwardRef( + ({ canSubmit }, ref) => { + const { pending } = useFormStatus() + + return ( +
+ {pending && ( + + )} + + +
+ ) + } +) + +SubmitButton.displayName = 'SubmitButton' + +interface AssistantChatFormProps { + chatContext: string + placeholder: string + canSubmit?: () => Promise +} + +const AssistantChatForm = ({ + chatContext, + placeholder, + canSubmit = () => Promise.resolve(true), +}: AssistantChatFormProps) => { + const { thread_id } = useParams() + const textAreaRef = useRef(null) + + const disabled = false + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!e.shiftKey && (e.key === 'Enter' || e.key === 'NumpadEnter')) { + e.preventDefault() + e.currentTarget.form?.requestSubmit() + } + } + + const handleChange = (e: ChangeEvent) => { + 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 ( + <> +
{ + const flag = await canSubmit() + if (flag) { + upsertMessageFormAction(formData) + } + }} + > +
+ + + 0} + isLoading={disabled} + /> + + {chatContext === 'new' && ( + { + if (textAreaRef?.current) textAreaRef.current.value = v + }} + /> + )} + + ) +} + +AssistantChatForm.displayName = 'AssistantChatForm' + +export { AssistantChatForm } diff --git a/apps/database-new/components/Auth/LoginForm.tsx b/apps/database-new/components/Auth/LoginForm.tsx index 6cf20b367a7..fbb755caa75 100644 --- a/apps/database-new/components/Auth/LoginForm.tsx +++ b/apps/database-new/components/Auth/LoginForm.tsx @@ -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') diff --git a/apps/database-new/components/Header/CurrentThreadName.tsx b/apps/database-new/components/Header/CurrentThreadName.tsx deleted file mode 100644 index 14622d7c0b7..00000000000 --- a/apps/database-new/components/Header/CurrentThreadName.tsx +++ /dev/null @@ -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 ( - //
- // {selectedMessage !== undefined && ( - //

- // {selectedMessage.text} - //

- // )} - //
- <> - ) -} - -export default CurrentThreadName diff --git a/apps/database-new/components/Header/Header.tsx b/apps/database-new/components/Header/Header.tsx index 6d10e899364..6ac40783cec 100644 --- a/apps/database-new/components/Header/Header.tsx +++ b/apps/database-new/components/Header/Header.tsx @@ -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 (
- -
- +
diff --git a/apps/database-new/components/Header/HeaderActions.tsx b/apps/database-new/components/Header/HeaderActions.tsx index 7d600e018af..27bf0e7c290 100644 --- a/apps/database-new/components/Header/HeaderActions.tsx +++ b/apps/database-new/components/Header/HeaderActions.tsx @@ -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) - - async function signout() { - const { error } = await supabase.auth.signOut() - if (error) { - console.log('Error logging out:', error.message) - return - } - - router.push('/') - setCurrentUser(null) - } - return (
{segment && segment.includes('thread') && ( @@ -46,12 +22,6 @@ const HeaderActions = ({ user }: HeaderActionsProps) => { - - {/* {currentUser ? ( - - ) : ( - - )} */}
) } diff --git a/apps/database-new/components/Header/SaveSchemaDropdown.tsx b/apps/database-new/components/Header/SaveSchemaDropdown.tsx index 6d46e5f9ef3..24a2b4121a5 100644 --- a/apps/database-new/components/Header/SaveSchemaDropdown.tsx +++ b/apps/database-new/components/Header/SaveSchemaDropdown.tsx @@ -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() diff --git a/apps/database-new/components/Loaders/GraphLoader.tsx b/apps/database-new/components/Loaders/GraphLoader.tsx index ea4e9a6b675..47c372ffcb3 100644 --- a/apps/database-new/components/Loaders/GraphLoader.tsx +++ b/apps/database-new/components/Loaders/GraphLoader.tsx @@ -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 (
diff --git a/apps/database-new/components/Loaders/SchemaLoader.tsx b/apps/database-new/components/Loaders/SchemaLoader.tsx index 3a47125e388..c8abf5ae796 100644 --- a/apps/database-new/components/Loaders/SchemaLoader.tsx +++ b/apps/database-new/components/Loaders/SchemaLoader.tsx @@ -1,6 +1,6 @@ import { Loader2 } from 'lucide-react' -const SchemaLoader = async () => { +const SchemaLoader = () => { return (
diff --git a/apps/database-new/app/[threadId]/[runId]/[messageId]/CodeEditor.utils.ts b/apps/database-new/components/MonacoEditor/CodeEditor.utils.ts similarity index 100% rename from apps/database-new/app/[threadId]/[runId]/[messageId]/CodeEditor.utils.ts rename to apps/database-new/components/MonacoEditor/CodeEditor.utils.ts diff --git a/apps/database-new/app/[threadId]/[runId]/[messageId]/MonacoEditor.tsx b/apps/database-new/components/MonacoEditor/index.tsx similarity index 100% rename from apps/database-new/app/[threadId]/[runId]/[messageId]/MonacoEditor.tsx rename to apps/database-new/components/MonacoEditor/index.tsx diff --git a/apps/database-new/app/[threadId]/[runId]/[messageId]/SchemaFlow.constants.ts b/apps/database-new/components/SchemaFlowHandler/SchemaFlow.constants.ts similarity index 100% rename from apps/database-new/app/[threadId]/[runId]/[messageId]/SchemaFlow.constants.ts rename to apps/database-new/components/SchemaFlowHandler/SchemaFlow.constants.ts diff --git a/apps/database-new/app/[threadId]/[runId]/[messageId]/SchemaFlow.utils.ts b/apps/database-new/components/SchemaFlowHandler/SchemaFlow.utils.ts similarity index 99% rename from apps/database-new/app/[threadId]/[runId]/[messageId]/SchemaFlow.utils.ts rename to apps/database-new/components/SchemaFlowHandler/SchemaFlow.utils.ts index fd2ea3af829..4b43c6287b9 100644 --- a/apps/database-new/app/[threadId]/[runId]/[messageId]/SchemaFlow.utils.ts +++ b/apps/database-new/components/SchemaFlowHandler/SchemaFlow.utils.ts @@ -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, diff --git a/apps/database-new/app/[threadId]/[runId]/[messageId]/TableNode.tsx b/apps/database-new/components/SchemaFlowHandler/TableNode.tsx similarity index 100% rename from apps/database-new/app/[threadId]/[runId]/[messageId]/TableNode.tsx rename to apps/database-new/components/SchemaFlowHandler/TableNode.tsx diff --git a/apps/database-new/app/[threadId]/[runId]/[messageId]/SchemaFlowHandler.tsx b/apps/database-new/components/SchemaFlowHandler/index.tsx similarity index 74% rename from apps/database-new/app/[threadId]/[runId]/[messageId]/SchemaFlowHandler.tsx rename to apps/database-new/components/SchemaFlowHandler/index.tsx index 1cb3d773a19..f9d96b516b7 100644 --- a/apps/database-new/app/[threadId]/[runId]/[messageId]/SchemaFlowHandler.tsx +++ b/apps/database-new/components/SchemaFlowHandler/index.tsx @@ -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 ( diff --git a/apps/database-new/components/Shimmer.tsx b/apps/database-new/components/Shimmer.tsx index 2a3aa3ae437..04262b7a294 100644 --- a/apps/database-new/components/Shimmer.tsx +++ b/apps/database-new/components/Shimmer.tsx @@ -1,6 +1,6 @@ const ANIMATION_DELAY = 150 -const ShimmeringLoader = ({ className = '', delayIndex = 0, animationDelay = 150 }) => { +const ShimmeringLoader = ({ className = '', delayIndex = 0, animationDelay = ANIMATION_DELAY }) => { return (
- useQuery({ - queryKey: [userId, 'conversations'], - enabled: enabled && !!userId, - queryFn: async () => { - // [Joshen] Just mocking the conversations data, to replace with fetching from Supabase - const getConversations = (): Promise => { - 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 - }, - }) diff --git a/apps/database-new/data/messages-query.ts b/apps/database-new/data/messages-query.ts deleted file mode 100644 index 8b8bd4ea34e..00000000000 --- a/apps/database-new/data/messages-query.ts +++ /dev/null @@ -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({ -// 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 -// }, -// }) diff --git a/apps/database-new/package.json b/apps/database-new/package.json index a57afb3a814..9a9fc8f6d3e 100644 --- a/apps/database-new/package.json +++ b/apps/database-new/package.json @@ -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", diff --git a/apps/database-new/supabase/migrations/20231122191507_get_started.sql b/apps/database-new/supabase/migrations/20231122191507_get_started.sql index f34d271efeb..3cbc13003f1 100644 --- a/apps/database-new/supabase/migrations/20231122191507_get_started.sql +++ b/apps/database-new/supabase/migrations/20231122191507_get_started.sql @@ -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 -) \ No newline at end of file +); + +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; \ No newline at end of file diff --git a/apps/database-new/supabase/migrations/20231204135129_remote_schema.sql b/apps/database-new/supabase/migrations/20231204135129_remote_schema.sql deleted file mode 100644 index d49cf309304..00000000000 --- a/apps/database-new/supabase/migrations/20231204135129_remote_schema.sql +++ /dev/null @@ -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)); - - - diff --git a/apps/database-new/supabase/migrations/20231204164321_remote_schema.sql b/apps/database-new/supabase/migrations/20231204164321_remote_schema.sql deleted file mode 100644 index 85b002d749a..00000000000 --- a/apps/database-new/supabase/migrations/20231204164321_remote_schema.sql +++ /dev/null @@ -1,3 +0,0 @@ -alter table "public"."threads" add column "latest_message_id" text; - - diff --git a/apps/database-new/types/supabase.ts b/apps/database-new/types/supabase.ts index 7050f57386e..b87b06631d0 100644 --- a/apps/database-new/types/supabase.ts +++ b/apps/database-new/types/supabase.ts @@ -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 diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/Message.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/Message.tsx index 99afc52868d..50e19c6ad65 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/Message.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/Message.tsx @@ -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 diff --git a/package-lock.json b/package-lock.json index f1036169ba9..0cdf0c4948b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/studio/components/grid/SupabaseGrid.tsx b/studio/components/grid/SupabaseGrid.tsx deleted file mode 100644 index 64524b14fbe..00000000000 --- a/studio/components/grid/SupabaseGrid.tsx +++ /dev/null @@ -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((props, ref) => { - const _props = cleanupProps(props) - - return ( - - - - - - ) -}) - -const SupabaseGridLayout = forwardRef( - 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(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) { - dispatch({ - type: 'ADD_NEW_ROW', - payload: row, - }) - }, - rowEdited(row: Dictionary, 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 ( -
-
- {showCustomChildren && children !== undefined ? ( - <>{children} - ) : ( - <> - - - )} - -
- ) - } -)