mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
chore: Remove database-design app (#29003)
Remove all files related to database-design app.
This commit is contained in:
1 parent
24f590e893
commit
5acbd4ab9f
102 files changed
+2
-7962
No files matched your search
@@ -1,5 +0,0 @@
|
||||
# Update these with your Supabase details from your project settings > API
|
||||
# https://app.supabase.com/project/_/settings/api
|
||||
NEXT_PUBLIC_SUPABASE_URL=your-project-url
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
||||
OPENAI_API_KEY=your-openai-api-key
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
extends: ['eslint-config-supabase/next'],
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -1,5 +0,0 @@
|
||||
# database.design
|
||||
|
||||
## Overview
|
||||
|
||||
This site will host a tool for designing SQL schemas.
|
||||
@@ -1,20 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useAppStateSnapshot } from '@/lib/state'
|
||||
import { usePathname, useParams } from 'next/navigation'
|
||||
import { LoadingLine as LoadingLineComponent } from 'ui'
|
||||
|
||||
function LoadingLine() {
|
||||
const snap = useAppStateSnapshot()
|
||||
const params = useParams()
|
||||
const pathName = usePathname()
|
||||
|
||||
const runId = params.runId as string
|
||||
|
||||
const loading =
|
||||
runId && snap.runsLoading.includes(runId) && pathName.includes(runId) ? true : false
|
||||
|
||||
return <LoadingLineComponent loading={loading} />
|
||||
}
|
||||
|
||||
export { LoadingLine }
|
||||
@@ -1,19 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useAppStateSnapshot } from '@/lib/state'
|
||||
import { DialogContent, Dialog } from 'ui'
|
||||
|
||||
const LoginDialog = ({ children }: { children: React.ReactNode }) => {
|
||||
const snap = useAppStateSnapshot()
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={snap.loginDialogOpen}
|
||||
onOpenChange={() => snap.setLoginDialogOpen(!snap.loginDialogOpen)}
|
||||
>
|
||||
<DialogContent className="max-w-md">{children}</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export { LoginDialog }
|
||||
@@ -1,34 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { AssistantChatForm } from '@/components/AssistantChatForm'
|
||||
import { useAppStateSnapshot } from '@/lib/state'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
|
||||
const NewThreadInput = () => {
|
||||
const supabase = createClient()
|
||||
const snap = useAppStateSnapshot()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative w-10/12 xl:w-11/12 max-w-xl">
|
||||
<AssistantChatForm
|
||||
key={'new-thread-form'}
|
||||
chatContext={'new'}
|
||||
canSubmit={async () => {
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
if (!user) {
|
||||
snap.setLoginDialogOpen(true)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}}
|
||||
placeholder="e.g Create a Telegram-like chat application"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewThreadInput
|
||||
@@ -1,19 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { createRef, useEffect } from 'react'
|
||||
|
||||
const BottomMarker = () => {
|
||||
const ref = createRef<HTMLDivElement>()
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
setTimeout(() => {
|
||||
if (ref.current) ref.current.scrollIntoView({ behavior: 'smooth' })
|
||||
}, 700)
|
||||
}
|
||||
}, [ref])
|
||||
|
||||
return <div ref={ref} />
|
||||
}
|
||||
|
||||
export { BottomMarker }
|
||||
@@ -1,23 +0,0 @@
|
||||
import { format } from 'sql-formatter'
|
||||
|
||||
import { MonacoEditor } from '@/components/MonacoEditor'
|
||||
import { CodeEditorContainer } from './CodeEditorContainer'
|
||||
|
||||
export async function CodeEditor({ promisedMessage }: { promisedMessage: Promise<string> }) {
|
||||
const code = await promisedMessage
|
||||
|
||||
// strip the ```sql ``` from the code
|
||||
// not sure why we need this & can't get openai to stop returning it
|
||||
const strippedCode = code.replace('```sql\n', '').replace('\n```', '')
|
||||
const formattedCode = format(strippedCode, { language: 'postgresql' })
|
||||
|
||||
/**
|
||||
* - CodeEditorContainer is a client component, which uses valtio state
|
||||
* - MonacoEditor is a server component injected into it
|
||||
*/
|
||||
return (
|
||||
<CodeEditorContainer>
|
||||
<MonacoEditor id="sql-editor" language="pgsql" value={formattedCode} />
|
||||
</CodeEditorContainer>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
import { useAppStateSnapshot } from '@/lib/state'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
function CodeEditorContainer({ children }: { children: ReactNode }) {
|
||||
const snap = useAppStateSnapshot()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
snap.hideCode ? 'max-w-0' : 'max-w-lg 2xl:max-w-xl',
|
||||
'w-full xl:border-l',
|
||||
'grow flex flex-col h-full'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { CodeEditorContainer }
|
||||
@@ -1,116 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { Message as MessageItem } from 'ai/react'
|
||||
import Link from 'next/link'
|
||||
import { useParams, usePathname } from 'next/navigation'
|
||||
import { cn } from 'ui'
|
||||
|
||||
interface MessageItemProps {
|
||||
message: MessageItem
|
||||
isLatest: boolean
|
||||
times: {
|
||||
hoursFromNow: number
|
||||
formattedTimeFromNow: string
|
||||
formattedCreatedAt: string
|
||||
}
|
||||
}
|
||||
|
||||
const MessageItem = ({ message, isLatest, times }: MessageItemProps) => {
|
||||
const { thread_id } = useParams()
|
||||
// using the local state for run loading
|
||||
const isLoading = false
|
||||
|
||||
const { hoursFromNow, formattedTimeFromNow, formattedCreatedAt } = times
|
||||
|
||||
// chat shown as selected when url matches
|
||||
const isSelected = usePathname().includes(message.id)
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
'group',
|
||||
'transition flex w-full gap-x-5 px-4 xl:px-8 hover:bg-surface-200/50 cursor-pointer border-r',
|
||||
isSelected && 'bg-surface-200',
|
||||
isSelected ? 'border-r-foreground' : 'border-r border-r-transparent'
|
||||
)}
|
||||
href={`/${thread_id}/${message.id}`}
|
||||
>
|
||||
<div className="flex flex-col justify-between items-center relative top-3">
|
||||
<div
|
||||
className={cn(
|
||||
'transition w-2.5 h-2.5 mt-[1px] ml-[1px] rounded-full border',
|
||||
isSelected
|
||||
? 'bg-dbnew border-dbnew'
|
||||
: 'bg-transparent border-foreground-muted group-hover:border-foreground'
|
||||
)}
|
||||
/>
|
||||
|
||||
{isLoading && (
|
||||
<span
|
||||
className={cn(
|
||||
'absolute w-4 h-4 -top-0.5',
|
||||
'after:content-spinner after:t-0 after:block after:absolute after:h-4 after:w-4 after:border-r after:border-r-dbnew after:rounded-[50%] after:rotate-45 z-10',
|
||||
'animate-spin'
|
||||
)}
|
||||
>
|
||||
<div className="absolute border w-4 h-4 rounded-full z-0" />
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Node line*/}
|
||||
{!isLatest && <div className="border-l border-strong flex-grow" />}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col gap-y-2 py-4">
|
||||
<div className="group relative">
|
||||
<span className="z-10 absolute top-0 -left-[8px]">
|
||||
<svg viewBox="0 0 8 13" height="13" width="8">
|
||||
<path
|
||||
className={
|
||||
// 'transition fill-background-surface-100 border'
|
||||
isSelected
|
||||
? 'transition stroke-border fill-background-surface-100 stroke-border-default'
|
||||
: 'transition stroke-border fill-background-surface-100 stroke-border-default'
|
||||
}
|
||||
d="M1.533,2.568L8,11.193V0L2.812,0C1.042,0,0.474,1.156,1.533,2.568z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div
|
||||
title={message.content}
|
||||
className={cn(
|
||||
'cursor-pointer transition relative overflow-hidden',
|
||||
'w-full rounded-lg rounded-tl-none',
|
||||
'bg-alternative',
|
||||
'border',
|
||||
isSelected ? 'bg-surface-100' : 'bg-surface-100 group-hover:bg-surface-200'
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className={cn(
|
||||
'transition p-4 text-xs',
|
||||
isSelected ? 'text-foreground' : 'text-light group-hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
{message.content}
|
||||
</p>
|
||||
{isLoading && <div className="chat-shimmering-loader w-full h-0.5 absolute bottom-0" />}
|
||||
</div>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<p
|
||||
className={cn(
|
||||
'transition-all',
|
||||
isSelected ? 'h-inherit opacity-100' : 'h-0 opacity-0',
|
||||
'font-mono text-xs text-foreground-lighter'
|
||||
)}
|
||||
>
|
||||
Sent {hoursFromNow > 6 ? `on ${formattedCreatedAt}` : formattedTimeFromNow}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default MessageItem
|
||||
@@ -1,62 +0,0 @@
|
||||
import { ScrollArea } from 'ui'
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { BottomMarker } from './BottomMarker'
|
||||
import { getMessages } from './getMessages'
|
||||
|
||||
import MessageItem from './MessageItem'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
export const Messages = async ({ threadId }: { threadId: string }) => {
|
||||
const { data: messages, error } = await getMessages(threadId)
|
||||
|
||||
if (error) {
|
||||
return <>Error happened</>
|
||||
}
|
||||
|
||||
const initialMessages = messages.flatMap((m) => [
|
||||
{
|
||||
id: m.message_id,
|
||||
content: m.message_input,
|
||||
role: 'user' as const,
|
||||
createdAt: new Date(m.created_at),
|
||||
},
|
||||
{
|
||||
id: m.message_id,
|
||||
content: m.message_content,
|
||||
role: 'assistant' as const,
|
||||
createdAt: new Date(m.created_at),
|
||||
},
|
||||
])
|
||||
|
||||
const userMessages = initialMessages.filter((m) => m.role === 'user')
|
||||
|
||||
return (
|
||||
<ScrollArea className="grow h-px">
|
||||
<div className="flex flex-col mb-6 xl:mb-12">
|
||||
{userMessages.map((message, idx) => {
|
||||
const createdAtTimestamp = dayjs(message.createdAt)
|
||||
const isLatest = Array.isArray(messages) && idx === messages.length - 1
|
||||
|
||||
const hoursFromNow = dayjs().diff(createdAtTimestamp, 'hour')
|
||||
const formattedTimeFromNow = dayjs(createdAtTimestamp).fromNow()
|
||||
|
||||
const formattedCreatedAt = dayjs(createdAtTimestamp).format('DD MMM YYYY, HH:mm')
|
||||
|
||||
const times = {
|
||||
hoursFromNow,
|
||||
formattedTimeFromNow,
|
||||
formattedCreatedAt,
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageItem key={message.id} message={message} isLatest={isLatest} times={times} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<BottomMarker />
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import SchemaFlowHandler from '@/components/SchemaFlowHandler'
|
||||
|
||||
export async function SchemaFlow({ promisedMessage }: { promisedMessage: Promise<string> }) {
|
||||
const code = await promisedMessage
|
||||
const strippedCode = code.replace('```sql\n', '').replace('\n```', '')
|
||||
|
||||
return <SchemaFlowHandler content={strippedCode} />
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
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 supabase = createClient()
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { cookies } from 'next/headers'
|
||||
import { cache } from 'react'
|
||||
|
||||
export const getMessages = cache(async (thread_id: string) => {
|
||||
const supabase = createClient()
|
||||
|
||||
return await supabase.from('messages').select().eq('thread_id', thread_id).order('created_at')
|
||||
})
|
||||
@@ -1,56 +0,0 @@
|
||||
import { AssistantChatForm } from '@/components/AssistantChatForm'
|
||||
import SaveSchemaDropdown from '@/components/Header/SaveSchemaDropdown'
|
||||
import ToggleCodeEditorButton from '@/components/Header/ToggleCodeEditorButton'
|
||||
import GraphLoader from '@/components/Loaders/GraphLoader'
|
||||
import SchemaLoader from '@/components/Loaders/SchemaLoader'
|
||||
import { cn } from '@ui/lib/utils/cn'
|
||||
import { Suspense } from 'react'
|
||||
import { CodeEditor } from './CodeEditor'
|
||||
import { Messages } from './Messages'
|
||||
import { SchemaFlow } from './SchemaFlow'
|
||||
import { getAssistantResponse } from './getAssistantMessage'
|
||||
|
||||
export interface ThreadPageProps {
|
||||
params: {
|
||||
thread_id: string
|
||||
message_id: string
|
||||
}
|
||||
}
|
||||
|
||||
export default async function ThreadPage({ params }: ThreadPageProps) {
|
||||
const promise = getAssistantResponse(params.thread_id, params.message_id)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col-reverse items-between xl:flex-row xl:items-center xl:justify-between bg-alternative h-full">
|
||||
<div
|
||||
className={cn(
|
||||
'bg',
|
||||
'h-full',
|
||||
'border-t xl:border-t-0 xl:border-r relative',
|
||||
'flex flex-col h-full border-r',
|
||||
'w-full xl:w-[400px] 2xl:w-[500px]'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col grow items-between">
|
||||
<Messages threadId={params.thread_id} />
|
||||
<div className="px-4 pb-4">
|
||||
<AssistantChatForm chatContext={'edit'} placeholder={'Any changes to make?'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="xl:hidden flex items-center gap-x-2 justify-end border-t py-2 px-2 bg-background">
|
||||
<ToggleCodeEditorButton />
|
||||
<SaveSchemaDropdown />
|
||||
</div>
|
||||
|
||||
<div className="grow max-h-screen flex flex-row items-center justify-between bg-alternative h-full">
|
||||
<Suspense fallback={<SchemaLoader />}>
|
||||
<SchemaFlow promisedMessage={promise} />
|
||||
</Suspense>
|
||||
<Suspense fallback={<GraphLoader />}>
|
||||
<CodeEditor promisedMessage={promise} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
'use server'
|
||||
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { Message } from 'ai'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { cookies } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
import { z } from 'zod'
|
||||
|
||||
export async function logout() {
|
||||
const supabase = createClient()
|
||||
|
||||
const { error } = await supabase.auth.signOut()
|
||||
|
||||
if (error) {
|
||||
console.log('Error logging out:', error.message)
|
||||
return
|
||||
}
|
||||
|
||||
revalidatePath('/', 'layout')
|
||||
redirect('/')
|
||||
}
|
||||
|
||||
//export async function createThread(prevState: any, formData: FormData) {
|
||||
export async function createThread(input: string, message: Message, currentThread?: string) {
|
||||
const supabase = createClient()
|
||||
|
||||
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 supabase = createClient()
|
||||
|
||||
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 supabase = createClient()
|
||||
|
||||
try {
|
||||
const schema = z.object({
|
||||
thread_id: z.string(),
|
||||
})
|
||||
|
||||
const data = schema.parse({
|
||||
thread_id: formData.get('thread_id'),
|
||||
})
|
||||
|
||||
await supabase.from('threads').delete().eq('id', data.thread_id)
|
||||
|
||||
revalidatePath('/profile')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to delete the thread',
|
||||
data: undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateThreadName(prevState: any, formData: FormData) {
|
||||
const supabase = createClient()
|
||||
|
||||
try {
|
||||
const schema = z.object({
|
||||
thread_title: z.string(),
|
||||
thread_id: z.string(),
|
||||
})
|
||||
|
||||
const data = schema.parse({
|
||||
thread_title: formData.get('thread_title'),
|
||||
thread_id: formData.get('thread_id'),
|
||||
})
|
||||
|
||||
const { error } = await supabase
|
||||
.from('threads')
|
||||
.update({ thread_title: data.thread_title })
|
||||
.eq('id', data.thread_id)
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
revalidatePath('/profile')
|
||||
|
||||
return {
|
||||
message: 'Thread name updated to ' + data.thread_title,
|
||||
success: true,
|
||||
data,
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to update title to update title',
|
||||
data: undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { cookies } from 'next/headers'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
// The `/auth/callback` route is required for the server-side auth flow implemented
|
||||
// by the Auth Helpers package. It exchanges an auth code for the user's session.
|
||||
// https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange
|
||||
const requestUrl = new URL(request.url)
|
||||
const code = requestUrl.searchParams.get('code')
|
||||
|
||||
if (code) {
|
||||
const supabase = createClient()
|
||||
await supabase.auth.exchangeCodeForSession(code)
|
||||
}
|
||||
|
||||
// URL to redirect to after sign in process completes
|
||||
return NextResponse.redirect(requestUrl.origin)
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { FAQS, faq } from '@/data/faqs'
|
||||
import { slugify } from '@/lib/utils'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'database.design | Faq',
|
||||
}
|
||||
|
||||
const FAQ = async () => {
|
||||
return (
|
||||
<div className="bg-background">
|
||||
<div className="py-16 bg-white ">
|
||||
<div className="max-w-screen-md px-4 mx-auto sm:px-6 lg:px-8">
|
||||
<h1 className="text-center font-display text-3xl font-bold text-black sm:text-5xl">
|
||||
Frequently Asked Questions
|
||||
</h1>
|
||||
<p className="mt-5 text-lg text-center text-foreground-light">
|
||||
Everything you have ever wondered about database.design
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center max-w-screen-md px-4 py-10 mx-auto sm:pt-20 sm:px-6 lg:px-8">
|
||||
{FAQS.map((faq: faq, index) => (
|
||||
<article
|
||||
key={index}
|
||||
className="prose prose-headings:scroll-mt-20 prose-headings:font-display prose-headings:font-semibold w-full"
|
||||
>
|
||||
<a href={`#${slugify(faq.question)}`} className="no-underline hover:underline">
|
||||
<h2 id={slugify(faq.question)}>{faq.question}</h2>
|
||||
</a>
|
||||
<p>{faq.answer}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FAQ
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@@ -1,199 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import '~ui/build/css/source/global.css';
|
||||
@import '~ui/build/css/themes/dark.css';
|
||||
@import '~ui/build/css/themes/light.css';
|
||||
|
||||
@font-face {
|
||||
font-family: 'circular';
|
||||
src:
|
||||
url(/fonts/custom/CustomFont-Book.woff2) format('woff2'),
|
||||
url(/fonts/custom/CustomFont-Book.woff) format('woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'circular';
|
||||
src:
|
||||
url(/fonts/custom/CustomFont-BookItalic.woff2) format('woff2'),
|
||||
url(/fonts/custom/CustomFont-BookItalic.woff) format('woff');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'circular';
|
||||
src:
|
||||
url(/fonts/custom/CustomFont-Medium.woff2) format('woff2'),
|
||||
url(/fonts/custom/CustomFont-Medium.woff) format('woff');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'circular';
|
||||
src:
|
||||
url(/fonts/custom/CustomFont-MediumItalic.woff2) format('woff2'),
|
||||
url(/fonts/custom/CustomFont-MediumItalic.woff) format('woff');
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'circular';
|
||||
src:
|
||||
url(/fonts/custom/CustomFont-Bold.woff2) format('woff2'),
|
||||
url(/fonts/custom/CustomFont-Bold.woff) format('woff');
|
||||
font-weight: 700;
|
||||
font-style: 600;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'circular';
|
||||
src:
|
||||
url(/fonts/custom/CustomFont-BoldItalic.woff2) format('woff2'),
|
||||
url(/fonts/custom/CustomFont-BoldItalic.woff) format('woff');
|
||||
font-style: 600;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'circular';
|
||||
src:
|
||||
url(/fonts/custom/CustomFont-Black.woff2) format('woff2'),
|
||||
url(/fonts/custom/CustomFont-Black.woff) format('woff');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'circular';
|
||||
src:
|
||||
url(/fonts/custom/CustomFont-BlackItalic.woff2) format('woff2'),
|
||||
url(/fonts/custom/CustomFont-BlackItalic.woff) format('woff');
|
||||
font-weight: 800;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'source code pro';
|
||||
src: url('/fonts/source-code-pro/SourceCodePro-Regular.eot');
|
||||
src:
|
||||
url('/fonts/source-code-pro/SourceCodePro-Regular.woff2') format('woff2'),
|
||||
url('/fonts/source-code-pro/SourceCodePro-Regular.woff') format('woff'),
|
||||
url('/fonts/source-code-pro/SourceCodePro-Regular.ttf') format('truetype'),
|
||||
url('/fonts/source-code-pro/SourceCodePro-Regular.svg#SourceCodePro-Regular') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
main {
|
||||
/* @apply bg-background;
|
||||
@apply text-foreground;
|
||||
height: 100%;
|
||||
width: 100%; */
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.chat-shimmering-loader {
|
||||
animation: shimmer 1.5s infinite linear;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
hsl(var(--background-default)) 0%,
|
||||
hsl(var(--brand-default)) 25%,
|
||||
hsl(var(--brand-300)) 35%,
|
||||
hsl(var(--background-default)) 45%,
|
||||
hsl(var(--background-surface-100)) 75%
|
||||
);
|
||||
background-size: 3000px 100%;
|
||||
}
|
||||
|
||||
.shimmering-loader {
|
||||
animation: shimmer 2s infinite linear;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
hsl(var(--border-default)) 4%,
|
||||
hsl(var(--background-surface-200)) 25%,
|
||||
hsl(var(--border-default)) 36%
|
||||
);
|
||||
background-size: 1000px 100%;
|
||||
}
|
||||
|
||||
.dark .shimmering-loader {
|
||||
animation: shimmer 2s infinite linear;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
hsl(var(--border-default)) 4%,
|
||||
hsl(var(--border-control)) 25%,
|
||||
hsl(var(--border-default)) 36%
|
||||
);
|
||||
background-size: 1000px 100%;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -1000px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.monaco-editor,
|
||||
.monaco-diff-editor {
|
||||
--vscode-editor-background: hsl(var(--background-alternative)) !important;
|
||||
--vscode-editorGutter-background: hsl(var(--background-alternative)) !important;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
/* .monaco-editor .overflow-guard */
|
||||
|
||||
.shimmering-loader {
|
||||
animation: shimmer 2s infinite linear;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
hsl(var(--border-default)) 4%,
|
||||
hsl(var(--background-surface-100)) 25%,
|
||||
hsl(var(--border-default)) 36%
|
||||
);
|
||||
background-size: 1000px 100%;
|
||||
}
|
||||
|
||||
.dark .shimmering-loader {
|
||||
animation: shimmer 2s infinite linear;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
hsl(var(--background-surface-100)) 4%,
|
||||
hsl(var(--border-default)) 25%,
|
||||
hsl(var(--background-surface-100)) 36%
|
||||
);
|
||||
background-size: 1000px 100%;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -1000px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
opacity: 0;
|
||||
animation-name: fadeIn;
|
||||
animation-duration: 500ms;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import '@ui/layout/ai-icon-animation/ai-icon-animation-style.module.css'
|
||||
import './globals.css'
|
||||
|
||||
import Footer from '@/components/Footer'
|
||||
import Header from '@/components/Header/Header'
|
||||
import { ThemeProvider } from '@/components/providers'
|
||||
import type { Metadata } from 'next'
|
||||
import { LoadingLine } from './LoadingLine'
|
||||
|
||||
const defaultUrl = process.env.VERCEL_URL
|
||||
? `https://${process.env.VERCEL_URL}`
|
||||
: 'http://localhost:3000'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(defaultUrl),
|
||||
title: 'database.design',
|
||||
description: 'Generate schemas from your ideas',
|
||||
}
|
||||
|
||||
// suppressHydrationWarning:
|
||||
// https://github.com/pacocoursey/next-themes#with-app
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" className="dark h-full bg-background" suppressHydrationWarning>
|
||||
<body className="flex flex-col h-full">
|
||||
<ThemeProvider defaultTheme="system" enableSystem disableTransitionOnChange>
|
||||
<Header />
|
||||
<LoadingLine />
|
||||
<main role="main" className="h-full w-full flex flex-col grow">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import LoginForm from '@/components/Auth/LoginForm'
|
||||
|
||||
export default function Login({ searchParams }: { searchParams: { message: string } }) {
|
||||
return <LoginForm searchParams={searchParams} />
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// import LoginForm from '@/components/Auth/LoginForm'
|
||||
import type { Metadata } from 'next'
|
||||
import { LoginDialog } from './LoginDialog'
|
||||
import NewThreadInput from './NewThreadInput'
|
||||
import LoginForm from '@/components/Auth/LoginForm'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'database.design | Create',
|
||||
}
|
||||
|
||||
const NewThread = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { prompt: string; message: string }
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="h-full flex items-center justify-center w-full flex-col gap-y-4">
|
||||
<NewThreadInput />
|
||||
</div>
|
||||
<LoginDialog>
|
||||
<LoginForm searchParams={searchParams} />
|
||||
</LoginDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewThread
|
||||
@@ -1,80 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { deleteThread } from '@/app/actions'
|
||||
import { createRef, useEffect } from 'react'
|
||||
import { useFormState, useFormStatus } from 'react-dom'
|
||||
import { Button, Input_Shadcn_, Modal } from 'ui'
|
||||
import { ThreadType } from './Threads'
|
||||
|
||||
const ConfirmDeleteThreadModal = ({
|
||||
thread,
|
||||
onClose,
|
||||
visible,
|
||||
}: {
|
||||
thread: ThreadType
|
||||
onClose: () => void
|
||||
visible: boolean
|
||||
}) => {
|
||||
const formRef = createRef<HTMLFormElement>()
|
||||
|
||||
const initialState = {
|
||||
message: '',
|
||||
success: false,
|
||||
data: {
|
||||
thread_id: thread.thread_id,
|
||||
},
|
||||
}
|
||||
|
||||
const [state, formAction] = useFormState(deleteThread, initialState)
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.success === true) {
|
||||
onClose()
|
||||
formRef.current?.reset()
|
||||
state.success = false
|
||||
}
|
||||
}, [state, onClose, formRef])
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.success === true) {
|
||||
onClose()
|
||||
formRef.current?.reset()
|
||||
state.success = false
|
||||
}
|
||||
}, [state, onClose, formRef])
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus()
|
||||
|
||||
return (
|
||||
<Button type="warning" htmlType="submit" aria-disabled={pending} loading={pending}>
|
||||
Delete thread
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="small"
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
hideFooter
|
||||
header="Confirm to delete thread?"
|
||||
className="pb-2"
|
||||
>
|
||||
<form action={formAction} key={`${thread.thread_id}-delete-thread-form`}>
|
||||
<Modal.Content className="py-4">
|
||||
<p className="text-sm">Once the thread is deleted, it cannot be recovered.</p>
|
||||
</Modal.Content>
|
||||
<Modal.Separator />
|
||||
<Input_Shadcn_ name="thread_id" value={state.data.thread_id} type="hidden" />
|
||||
<Modal.Content className="flex flex-row gap-3 justify-end">
|
||||
<Button type="default">Cancel</Button>
|
||||
<SubmitButton />
|
||||
</Modal.Content>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConfirmDeleteThreadModal
|
||||
@@ -1,83 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { updateThreadName } from '@/app/actions'
|
||||
import { createRef, useEffect } from 'react'
|
||||
import { useFormState, useFormStatus } from 'react-dom'
|
||||
import { Button, Input_Shadcn_, Label_Shadcn_, Modal } from 'ui'
|
||||
import { ThreadType } from './Threads'
|
||||
|
||||
const EditThreadModal = ({
|
||||
thread,
|
||||
onClose,
|
||||
visible,
|
||||
}: {
|
||||
thread: ThreadType
|
||||
onClose: () => void
|
||||
visible: boolean
|
||||
}) => {
|
||||
const formRef = createRef<HTMLFormElement>()
|
||||
|
||||
const initialState = {
|
||||
message: undefined,
|
||||
success: undefined,
|
||||
data: {
|
||||
thread_id: thread.thread_id,
|
||||
thread_title: thread.thread_title,
|
||||
},
|
||||
}
|
||||
|
||||
const [state, formAction] = useFormState(updateThreadName, initialState)
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.success === true) {
|
||||
onClose()
|
||||
formRef.current?.reset()
|
||||
state.success = undefined
|
||||
}
|
||||
}, [state, onClose, formRef])
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus()
|
||||
|
||||
return (
|
||||
<Button type="secondary" htmlType="submit" aria-disabled={pending} loading={pending}>
|
||||
Update thread name
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
alignFooter="right"
|
||||
size="medium"
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
hideFooter
|
||||
header="Edit thread name"
|
||||
className="pb-2"
|
||||
>
|
||||
<form ref={formRef} action={formAction} key={`${thread.thread_id}-update-thread-title-form`}>
|
||||
<Modal.Content className="py-4">
|
||||
<Label_Shadcn_ htmlFor="thread_title">Provide a name for your thread</Label_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
placeholder="Type in a name for the thread..."
|
||||
type="text"
|
||||
name="thread_title"
|
||||
defaultValue={state.data.thread_title}
|
||||
/>
|
||||
<Input_Shadcn_ name="thread_id" value={state.data.thread_id} type="hidden" />
|
||||
</Modal.Content>
|
||||
<Modal.Separator />
|
||||
<Modal.Content className="flex flex-row gap-3 justify-end">
|
||||
<Button type="default">Cancel</Button>
|
||||
<SubmitButton />
|
||||
<p aria-live="polite" className="sr-only" role="status">
|
||||
{state?.message}
|
||||
</p>
|
||||
</Modal.Content>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditThreadModal
|
||||
@@ -1,17 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { Button } from 'ui'
|
||||
|
||||
const EmptyState = () => {
|
||||
return (
|
||||
<div className="border rounded py-6 flex flex-col items-center justify-center gap-y-2">
|
||||
<p className="text-sm text-foreground-light">No conversations created yet</p>
|
||||
<Button type="default">
|
||||
<Link href="/">Start a conversation</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmptyState
|
||||
@@ -1,85 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { timeAgo } from '@/lib/utils'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconEdit2,
|
||||
IconMoreVertical,
|
||||
IconTrash2,
|
||||
} from 'ui'
|
||||
import ConfirmDeleteThreadModal from './ConfirmDeleteThreadModal'
|
||||
import EditThreadModal from './EditThreadModal'
|
||||
import { ThreadType } from './Threads'
|
||||
|
||||
const Thread = ({ thread }: { thread: ThreadType }) => {
|
||||
const { created_at, thread_id, thread_title } = thread
|
||||
const formattedTimeAgo = timeAgo(created_at!)
|
||||
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
const [deleteOpen, setDeleteOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
key={thread_id}
|
||||
className="group w-full flex flex-row items-center gap-6 border rounded pl-5 pr-2 transition bg-surface-100 hover:bg-surface-200 h-14"
|
||||
>
|
||||
<div className="flex flex-col grow overflow-hidden">
|
||||
<Link
|
||||
className="flex text-sm group-hover:underline truncate"
|
||||
href={`/${thread.thread_id}/${thread.message_id}`}
|
||||
>
|
||||
<span className="truncate">{thread_title}</span>
|
||||
</Link>
|
||||
<span className="text-xs text-foreground-lighter font-mono">
|
||||
Last updated {formattedTimeAgo}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex shrink">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="px-1 group">
|
||||
<IconMoreVertical size={14} className="text-light group-hover:text" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-32" align="end">
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onClick={() => {
|
||||
console.log('trying to edit thread')
|
||||
setEditOpen(true)
|
||||
}}
|
||||
>
|
||||
<IconEdit2 size={14} />
|
||||
<p>Edit name</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => setDeleteOpen(true)}>
|
||||
<IconTrash2 size={14} />
|
||||
<p>Delete thread</p>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmDeleteThreadModal
|
||||
key={`${thread_id}-delete-dialog`}
|
||||
thread={thread}
|
||||
visible={deleteOpen}
|
||||
onClose={() => setDeleteOpen(false)}
|
||||
/>
|
||||
<EditThreadModal
|
||||
key={`${thread_id}-edit-dialog`}
|
||||
thread={thread}
|
||||
visible={editOpen}
|
||||
onClose={() => setEditOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Thread
|
||||
@@ -1,38 +0,0 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { Database } from '@/types/supabase'
|
||||
import { cookies } from 'next/headers'
|
||||
import EmptyState from './EmptyState'
|
||||
import Thread from './Thread'
|
||||
|
||||
export type ThreadType = Database['public']['Views']['profile_threads']['Row']
|
||||
|
||||
async function Threads() {
|
||||
const supabase = createClient()
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (!user) return <p>Error fetching user details</p>
|
||||
|
||||
const { data } = await supabase
|
||||
.from('profile_threads')
|
||||
.select()
|
||||
.eq('user_id', user.id)
|
||||
.order('created_at', { ascending: false })
|
||||
const threads = data ?? []
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{threads.length > 0 ? (
|
||||
threads
|
||||
.sort()
|
||||
.map((thread) => <Thread key={`thread-item-${thread.thread_id}`} thread={thread} />)
|
||||
) : (
|
||||
<EmptyState />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Threads
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return <div className="px-4 xl:px-0 xl:max-w-4xl mx-auto h-full">{children}</div>
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { default as dayjs, default as relativeTime } from 'dayjs'
|
||||
import { cookies } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
import Threads from './Threads'
|
||||
import { Suspense } from 'react'
|
||||
import ShimmeringLoader from '@/components/Shimmer'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
export const metadata: Metadata = {
|
||||
title: 'database.design | Profile',
|
||||
}
|
||||
|
||||
// revalidatePath not working in the create route handler
|
||||
// force-dynamic to refetch every time if needed
|
||||
// just a hack for now
|
||||
// export const dynamic = 'force-dynamic'
|
||||
|
||||
const Profile = async () => {
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-x-8 py-6 xl:py-12 gap-y-6 xl:gap-y-0">
|
||||
<div className="col-span-4 xl:col-span-1 flex flex-col gap-y-6">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<Suspense fallback={<p>Loading user...</p>}>
|
||||
<ProfileDetails />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-4 xl:col-span-3 flex flex-col gap-y-4">
|
||||
<h2>Past conversations</h2>
|
||||
|
||||
<div className="w-full h-px border-t" />
|
||||
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex flex-col gap-4">
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded" />
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded" />
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded" />
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded opacity-50" />
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded opacity-25" />
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded opacity-10" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Threads />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
async function ProfileDetails() {
|
||||
const supabase = createClient()
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (!user) redirect('/')
|
||||
|
||||
return !user ? (
|
||||
// make this better!
|
||||
<p>No user found</p>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className="border border-foreground-lighter rounded-full w-12 h-12 bg-no-repeat bg-center bg-cover"
|
||||
style={{ backgroundImage: `url('${user.user_metadata.avatar_url}')` }}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-lg">{user.user_metadata.full_name}</p>
|
||||
<p className="text-foreground-light">@{user.user_metadata.user_name}</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Profile
|
||||
@@ -1,2 +0,0 @@
|
||||
export const NODE_WIDTH = 700
|
||||
export const NODE_HEIGHT = 200
|
||||
@@ -1,127 +0,0 @@
|
||||
// 'use client'
|
||||
|
||||
// import { useQuery } from '@tanstack/react-query'
|
||||
// import { Modal } from 'ui'
|
||||
// import { useParams } from 'next/navigation'
|
||||
// import { AssistantMessage, ReadThreadAPIResult, UserMessage } from '@/lib/types'
|
||||
// import { useEffect, useMemo } from 'react'
|
||||
// import { sortBy } from 'lodash'
|
||||
// import ReactFlow, {
|
||||
// Background,
|
||||
// BackgroundVariant,
|
||||
// ReactFlowProvider,
|
||||
// useReactFlow,
|
||||
// MiniMap,
|
||||
// } from 'reactflow'
|
||||
// import { getGraphDataFromMessages } from './AllThreadsModal.utils'
|
||||
// import MessageNode from './MessageNode'
|
||||
|
||||
// interface AllThreadsProps {
|
||||
// visible: boolean
|
||||
// onClose: () => void
|
||||
// onSelectMessage: (messageId: string, replyId: string) => void
|
||||
// }
|
||||
|
||||
// // [Joshen] POC idea for forking/branching - not for phase 1
|
||||
|
||||
// const AllThreads = ({ visible, onClose, onSelectMessage }: AllThreadsProps) => {
|
||||
// const params = useParams()
|
||||
// const reactFlowInstance = useReactFlow()
|
||||
// const nodeTypes = useMemo(() => ({ message: MessageNode }), [])
|
||||
|
||||
// // [Joshen] Scaffolding a query cause im presuming we need a different endpoint to retrieve _all_ threads
|
||||
// const { data, isSuccess } = useQuery<ReadThreadAPIResult>({
|
||||
// queryFn: async () => {
|
||||
// const response = await fetch(`/api/ai/sql/threads/${params.threadId}/read/${params.runId}`, {
|
||||
// method: 'GET',
|
||||
// })
|
||||
|
||||
// const result = await response.json()
|
||||
// return result
|
||||
// },
|
||||
// queryKey: [params.threadId, params.runId],
|
||||
// refetchInterval: (options) => {
|
||||
// const data = options.state.data
|
||||
// if (data && data.status === 'completed') {
|
||||
// return Infinity
|
||||
// }
|
||||
// return 5000
|
||||
// },
|
||||
// enabled: !!(params.threadId && params.runId),
|
||||
// })
|
||||
|
||||
// const messages = useMemo(() => {
|
||||
// if (isSuccess) return sortBy(data.messages, (m) => m.created_at)
|
||||
// return []
|
||||
// }, [data?.messages, isSuccess])
|
||||
|
||||
// const userMessages = messages.filter((m) => m.role === 'user') as UserMessage[]
|
||||
|
||||
// useEffect(() => {
|
||||
// if (reactFlowInstance !== undefined && userMessages.length > 0) {
|
||||
// const { nodes, edges } = getGraphDataFromMessages({
|
||||
// messages: userMessages,
|
||||
// onSelectMessage: (message: UserMessage) => {
|
||||
// const index = messages.indexOf(message)
|
||||
// const reply = messages[index + 1] as AssistantMessage
|
||||
// onSelectMessage(message.id, reply.id)
|
||||
// },
|
||||
// })
|
||||
// reactFlowInstance.setNodes(nodes)
|
||||
// reactFlowInstance.setEdges(edges)
|
||||
// setTimeout(() => {
|
||||
// reactFlowInstance.fitView({})
|
||||
// const viewport = reactFlowInstance.getViewport()
|
||||
// reactFlowInstance.setViewport({ x: viewport.x - 70, y: 150, zoom: 1 })
|
||||
// })
|
||||
// }
|
||||
// }, [reactFlowInstance, userMessages, messages, onSelectMessage])
|
||||
|
||||
// useEffect(() => {
|
||||
// if (reactFlowInstance !== undefined && visible) {
|
||||
// reactFlowInstance.fitView({})
|
||||
// }
|
||||
// }, [reactFlowInstance, visible])
|
||||
|
||||
// return (
|
||||
// <Modal
|
||||
// showCloseButton
|
||||
// hideFooter
|
||||
// size="xxlarge"
|
||||
// visible={visible}
|
||||
// onCancel={onClose}
|
||||
// header="All threads in current conversation"
|
||||
// >
|
||||
// <div className="h-[700px] border-t bg-background">
|
||||
// <ReactFlow
|
||||
// nodesDraggable={false}
|
||||
// edgesUpdatable={false}
|
||||
// defaultNodes={[]}
|
||||
// defaultEdges={[]}
|
||||
// maxZoom={1.1}
|
||||
// nodeTypes={nodeTypes}
|
||||
// proOptions={{ hideAttribution: true }}
|
||||
// >
|
||||
// <Background gap={16} color="#000" variant={BackgroundVariant.Lines} />
|
||||
// <MiniMap
|
||||
// pannable
|
||||
// zoomable
|
||||
// nodeColor={'rgba(0,0,0,0.7)'}
|
||||
// maskColor={'rgba(0,0,0,0.85)'}
|
||||
// className="border border-control rounded-md shadow-sm"
|
||||
// />
|
||||
// </ReactFlow>
|
||||
// </div>
|
||||
// </Modal>
|
||||
// )
|
||||
// }
|
||||
|
||||
// const AllThreadsModal = ({ visible, onClose, onSelectMessage }: AllThreadsProps) => {
|
||||
// return (
|
||||
// <ReactFlowProvider>
|
||||
// <AllThreads visible={visible} onClose={onClose} onSelectMessage={onSelectMessage} />
|
||||
// </ReactFlowProvider>
|
||||
// )
|
||||
// }
|
||||
|
||||
// export default AllThreadsModal
|
||||
@@ -1,65 +0,0 @@
|
||||
// import { UserMessage } from '@/lib/types'
|
||||
// import dagre from '@dagrejs/dagre'
|
||||
// import { Edge, Node, Position } from 'reactflow'
|
||||
// import { NODE_HEIGHT, NODE_WIDTH } from './AllThreadsModal.constants'
|
||||
// import { MessageNodeData } from './MessageNode'
|
||||
|
||||
// export const getGraphDataFromMessages = ({
|
||||
// messages,
|
||||
// onSelectMessage,
|
||||
// }: {
|
||||
// messages: UserMessage[]
|
||||
// onSelectMessage: (message: UserMessage) => void
|
||||
// }): { nodes: Node<MessageNodeData>[]; edges: Edge[] } => {
|
||||
// const nodes: Node[] = messages.map((message, idx) => {
|
||||
// return {
|
||||
// id: message.id,
|
||||
// type: 'message',
|
||||
// data: {
|
||||
// id: message.id,
|
||||
// text: message.text,
|
||||
// isStart: idx === 0,
|
||||
// isEnd: idx === messages.length - 1,
|
||||
// onSelectMessage: () => onSelectMessage(message),
|
||||
// },
|
||||
// position: { x: 0, y: 0 },
|
||||
// }
|
||||
// })
|
||||
|
||||
// const edges: Edge[] = []
|
||||
// messages.forEach((message, idx) => {
|
||||
// if (idx > 0) {
|
||||
// edges.push({
|
||||
// id: `edge-${idx}`,
|
||||
// source: messages[idx - 1].id,
|
||||
// target: message.id,
|
||||
// type: 'smoothstep',
|
||||
// animated: true,
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
|
||||
// const dagreGraph = new dagre.graphlib.Graph()
|
||||
// dagreGraph.setDefaultEdgeLabel(() => ({}))
|
||||
// dagreGraph.setGraph({ rankdir: 'TB', ranksep: 50, nodesep: 20 })
|
||||
|
||||
// nodes.forEach((node) =>
|
||||
// dagreGraph.setNode(node.id, { width: NODE_WIDTH / 2, height: NODE_HEIGHT / 2 })
|
||||
// )
|
||||
// edges.forEach((edge) => dagreGraph.setEdge(edge.source, edge.target))
|
||||
// dagre.layout(dagreGraph)
|
||||
|
||||
// nodes.forEach((node) => {
|
||||
// const nodeWithPosition = dagreGraph.node(node.id)
|
||||
// node.targetPosition = Position.Top
|
||||
// node.sourcePosition = Position.Bottom
|
||||
// node.position = {
|
||||
// x: nodeWithPosition.x - nodeWithPosition.width / 2,
|
||||
// y: nodeWithPosition.y - nodeWithPosition.height / 2,
|
||||
// }
|
||||
|
||||
// return node
|
||||
// })
|
||||
|
||||
// return { nodes, edges }
|
||||
// }
|
||||
@@ -1,44 +0,0 @@
|
||||
// import { Handle, NodeProps, Position } from 'reactflow'
|
||||
// import { NODE_HEIGHT, NODE_WIDTH } from './AllThreadsModal.constants'
|
||||
|
||||
// export interface MessageNodeData {
|
||||
// id: string
|
||||
// text: string
|
||||
// isStart: boolean
|
||||
// isEnd: boolean
|
||||
// onSelectMessage: () => void
|
||||
// }
|
||||
|
||||
// const MessageNode = ({ data }: NodeProps<MessageNodeData>) => {
|
||||
// const { id, text, isStart, isEnd, onSelectMessage } = data
|
||||
// return (
|
||||
// <>
|
||||
// {!isStart && (
|
||||
// <Handle
|
||||
// type="target"
|
||||
// id="handle-t"
|
||||
// position={Position.Top}
|
||||
// style={{ background: 'transparent' }}
|
||||
// />
|
||||
// )}
|
||||
// <div
|
||||
// className="flex flex-col gap-y-1 rounded bg-surface-100 border border-default p-3 hover:bg-surface-200 transition cursor-pointer"
|
||||
// style={{ width: NODE_WIDTH / 2 - 10, height: NODE_HEIGHT / 2 }}
|
||||
// onClick={() => onSelectMessage()}
|
||||
// >
|
||||
// <p className="text-xs text-foreground-light font-mono">{id}</p>
|
||||
// <p className="text-sm">{text}</p>
|
||||
// </div>
|
||||
// {!isEnd && (
|
||||
// <Handle
|
||||
// type="source"
|
||||
// id="handle-s"
|
||||
// position={Position.Bottom}
|
||||
// style={{ background: 'transparent' }}
|
||||
// />
|
||||
// )}
|
||||
// </>
|
||||
// )
|
||||
// }
|
||||
|
||||
// export default MessageNode
|
||||
@@ -1,37 +0,0 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
// something a little funny here maybe?
|
||||
const textArray = [
|
||||
"We're working hard to compute your results",
|
||||
'Installing a new flux capacitor',
|
||||
'Reconfiguring the warp drive',
|
||||
'Analyzing the space modulator',
|
||||
'etc....',
|
||||
]
|
||||
|
||||
// this is just an idea.
|
||||
// maybe a bad one
|
||||
// needs better animation if we're keeping it
|
||||
const ChatLoadingAnimation = () => {
|
||||
const [currentTextIndex, setCurrentTextIndex] = useState(0)
|
||||
const [currentText, setCurrentText] = useState(textArray[0])
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentTextIndex((prevIndex) => (prevIndex + 1) % textArray.length)
|
||||
setCurrentText(textArray[currentTextIndex])
|
||||
}, 2000)
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [currentTextIndex, textArray])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{currentText}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatLoadingAnimation
|
||||
@@ -1,31 +0,0 @@
|
||||
import { CHAT_EXAMPLES } from '@/data/chat-examples'
|
||||
import { cn } from '@ui/lib/utils/cn'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
|
||||
export const ChatSuggestions = ({ setInput }: { setInput: (s: string) => void }) => {
|
||||
const suggestions = CHAT_EXAMPLES
|
||||
return (
|
||||
<div className="flex gap-3 mt-4">
|
||||
{suggestions.map((suggestion, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
type="button"
|
||||
className={cn(
|
||||
'text-xs',
|
||||
'flex items-center gap-3 !pr-3',
|
||||
'transition border rounded-full px-3 py-1.5',
|
||||
'text-light',
|
||||
'hover:border-stronger hover:text'
|
||||
)}
|
||||
onClick={(event) => {
|
||||
setInput(suggestion.prompt)
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
{suggestion.label}
|
||||
<ExternalLink size={12} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
'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)
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { ChangeEvent, FormHTMLAttributes, ReactNode, forwardRef, useRef } from 'react'
|
||||
import { useFormStatus } from 'react-dom'
|
||||
import { TextArea_Shadcn_, cn } from 'ui'
|
||||
|
||||
import { ChatSuggestions } from './ChatSuggestions'
|
||||
import { upsertMessageFormAction } from './action'
|
||||
|
||||
export interface FormProps extends FormHTMLAttributes<HTMLFormElement> {
|
||||
loading?: boolean
|
||||
disabled?: boolean
|
||||
message?: string
|
||||
children?: ReactNode
|
||||
chatContext: 'new' | 'edit'
|
||||
}
|
||||
|
||||
const SubmitButton = forwardRef<HTMLButtonElement, { isLoading: boolean; canSubmit: boolean }>(
|
||||
({ canSubmit }, ref) => {
|
||||
const { pending } = useFormStatus()
|
||||
|
||||
return (
|
||||
<div className="absolute right-1.5 top-1.5 flex gap-3 items-center">
|
||||
{pending && (
|
||||
<Loader2 size={22} className="animate-spin w-7 h-7 text-muted" strokeWidth={1} />
|
||||
)}
|
||||
|
||||
<button
|
||||
title="Send AI prompt"
|
||||
ref={ref}
|
||||
type="submit"
|
||||
disabled={pending || canSubmit}
|
||||
className={cn(
|
||||
'transition-all',
|
||||
'flex items-center justify-center w-7 h-7 border border-control rounded-full mr-0.5 p-1.5 background-alternative',
|
||||
canSubmit ? 'text-default opacity-100' : 'text-muted opacity-50',
|
||||
pending ? 'hidden' : ''
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M13.5 3V2.25H15V3V10C15 10.5523 14.5522 11 14 11H3.56062L5.53029 12.9697L6.06062 13.5L4.99996 14.5607L4.46963 14.0303L1.39641 10.9571C1.00588 10.5666 1.00588 9.93342 1.39641 9.54289L4.46963 6.46967L4.99996 5.93934L6.06062 7L5.53029 7.53033L3.56062 9.5H13.5V3Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
SubmitButton.displayName = 'SubmitButton'
|
||||
|
||||
interface AssistantChatFormProps {
|
||||
chatContext: string
|
||||
placeholder: string
|
||||
canSubmit?: () => Promise<boolean>
|
||||
}
|
||||
|
||||
const AssistantChatForm = ({
|
||||
chatContext,
|
||||
placeholder,
|
||||
canSubmit = () => Promise.resolve(true),
|
||||
}: AssistantChatFormProps) => {
|
||||
const { thread_id } = useParams()
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const disabled = false
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (!e.shiftKey && (e.key === 'Enter' || e.key === 'NumpadEnter')) {
|
||||
e.preventDefault()
|
||||
e.currentTarget.form?.requestSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const target = e.target
|
||||
let newHeight = target.scrollHeight + 'px'
|
||||
const input = target.value
|
||||
if (!input) {
|
||||
newHeight = '40px'
|
||||
}
|
||||
if (target.style.height !== newHeight) {
|
||||
target.style.height = newHeight
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="relative"
|
||||
action={async (formData) => {
|
||||
const flag = await canSubmit()
|
||||
if (flag) {
|
||||
upsertMessageFormAction(formData)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={cn('absolute', 'top-2 left-2', 'ml-1 w-6 h-6 rounded-full bg-dbnew')}></div>
|
||||
<input hidden name="threadId" defaultValue={thread_id} />
|
||||
<TextArea_Shadcn_
|
||||
name="prompt"
|
||||
ref={textAreaRef}
|
||||
autoFocus
|
||||
rows={1}
|
||||
defaultValue=""
|
||||
disabled={disabled}
|
||||
contentEditable
|
||||
required
|
||||
className={
|
||||
'transition-all text-sm pl-12 pr-10 rounded-[18px] resize-none box-border leading-6'
|
||||
}
|
||||
style={{ height: '40px' }}
|
||||
placeholder={placeholder}
|
||||
spellCheck={false}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<SubmitButton
|
||||
canSubmit={(textAreaRef?.current?.value || '').length > 0}
|
||||
isLoading={disabled}
|
||||
/>
|
||||
</form>
|
||||
{chatContext === 'new' && (
|
||||
<ChatSuggestions
|
||||
setInput={(v) => {
|
||||
if (textAreaRef?.current) textAreaRef.current.value = v
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
AssistantChatForm.displayName = 'AssistantChatForm'
|
||||
|
||||
export { AssistantChatForm }
|
||||
@@ -1,64 +0,0 @@
|
||||
import { Button } from '@ui/components/Button'
|
||||
import { Github } from 'lucide-react'
|
||||
import { cookies, headers } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
|
||||
export default function LoginForm({ searchParams }: { searchParams?: { message?: string } }) {
|
||||
const signUp = async () => {
|
||||
'use server'
|
||||
|
||||
const origin = headers().get('origin')
|
||||
|
||||
const supabase = createClient()
|
||||
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'github',
|
||||
options: { redirectTo: `${origin}/auth/callback` },
|
||||
})
|
||||
|
||||
if (error) {
|
||||
console.log(error.message)
|
||||
return redirect(`/login?message=${error.message}`)
|
||||
} else {
|
||||
return redirect(data.url)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center h-full w-full px-8 sm:max-w-sm gap-2 mx-auto">
|
||||
<form
|
||||
className="animate-in flex-1 flex flex-col w-full justify-center gap-2 text-foreground"
|
||||
action={signUp}
|
||||
>
|
||||
<div className="flex items-center gap-x-2 justify-center mb-4">
|
||||
<span className="text-foreground-light">Sign in to</span>{' '}
|
||||
<div className="flex items-center gap-x-1.5 font-mono font-bold">
|
||||
<span>database</span>
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-purple-900"></div>
|
||||
<span>design</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2 mb-4">
|
||||
<p className="text-center text-sm ">You need to sign in to generate a schema</p>
|
||||
<p className="text-center text-xs">Takes just a few seconds</p>
|
||||
</div>
|
||||
<Button
|
||||
className="border text-sm bg-surface-100 rounded-md px-4 py-2 text-foreground mb-2 flex items-center justify-center gap-x-2"
|
||||
type="default"
|
||||
htmlType="submit"
|
||||
icon={<Github size={18} />}
|
||||
>
|
||||
Sign in with Github
|
||||
</Button>
|
||||
|
||||
{searchParams?.message && (
|
||||
<p className="mt-4 p-4 bg-foreground/10 text-foreground text-center">
|
||||
{searchParams.message}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import ThemeSwitcher from './Header/ThemeSwitcher'
|
||||
|
||||
export const links = [
|
||||
{ title: ` © Supabase`, url: 'https://supabase.com/' },
|
||||
{ title: 'FAQs', url: '/faq' },
|
||||
{ title: 'Open Source', url: 'https://supabase.com/open-source' },
|
||||
{ title: 'Privacy Settings', url: 'https://supabase.com/privacy' },
|
||||
]
|
||||
|
||||
const Footer = () => (
|
||||
<footer className="border-t py-4 w-full px-4 flex justify-between" role="contentinfo">
|
||||
<ul className="flex items-center gap-4 text-xs" role="menu">
|
||||
{links.map((link, index) => (
|
||||
<li key={index} role="menuitem">
|
||||
<Link href={link.url}>{link.title}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<ThemeSwitcher />
|
||||
</footer>
|
||||
)
|
||||
|
||||
export default Footer
|
||||
@@ -1,55 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { logout } from '@/app/actions'
|
||||
import { User } from '@supabase/supabase-js'
|
||||
import { HelpCircle, LogOut } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, IconUser } from 'ui'
|
||||
|
||||
interface AvatarDropdownProps {
|
||||
user: User
|
||||
}
|
||||
|
||||
export default function AvatarDropdown({ user }: AvatarDropdownProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="px-2 py-2">
|
||||
<p className="text-sm text-foreground">{user.user_metadata.full_name}</p>
|
||||
<p className="text-sm text-foreground-light">{user.user_metadata.email}</p>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<Link href="/profile">
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => {}}>
|
||||
<IconUser size={14} strokeWidth={1.5} className="text-lighter" />
|
||||
<p>Profile</p>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link href="/faq">
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => {}}>
|
||||
<HelpCircle size={14} strokeWidth={1.5} className="text-lighter" />
|
||||
<p>FAQs</p>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<a href="https://supabase.com" target="_blank" rel="noreferrer">
|
||||
<DropdownMenuItem className="space-x-2">
|
||||
<Image alt="supabase" src="/supabase.png" width={14} height={14} />
|
||||
<p>Supabase</p>
|
||||
</DropdownMenuItem>
|
||||
</a>
|
||||
</DropdownMenuGroup>
|
||||
<form action={logout}>
|
||||
<DropdownMenuItem className="space-x-2 w-full" asChild>
|
||||
<button type="submit">
|
||||
<LogOut size={14} strokeWidth={1.5} className="text-lighter" />
|
||||
<p>Sign out</p>
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import HeaderActions from './HeaderActions'
|
||||
import UserDropdown from './UserDropdown'
|
||||
|
||||
const Header = async () => {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
className="bg-background border flex items-center justify-between px-4 min-h-[50px]"
|
||||
>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<Link href="/">
|
||||
<div className="flex items-center gap-x-1.5 font-mono">
|
||||
<span>database</span>
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-dbnew"></div>
|
||||
<span>design</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<HeaderActions />
|
||||
<UserDropdown />
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
@@ -1,29 +0,0 @@
|
||||
'use client'
|
||||
import Link from 'next/link'
|
||||
import { useSelectedLayoutSegment } from 'next/navigation'
|
||||
import { Button } from 'ui'
|
||||
|
||||
import SaveSchemaDropdown from './SaveSchemaDropdown'
|
||||
import ToggleCodeEditorButton from './ToggleCodeEditorButton'
|
||||
|
||||
const HeaderActions = () => {
|
||||
const segment = useSelectedLayoutSegment()
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-2">
|
||||
{segment && segment.includes('thread') && (
|
||||
<div className="hidden xl:flex items-center gap-x-2">
|
||||
<ToggleCodeEditorButton />
|
||||
<SaveSchemaDropdown />
|
||||
<div className="border-r py-3" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button type="default" className="hidden xl:block">
|
||||
<Link href="/">New conversation</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeaderActions
|
||||
@@ -1,32 +0,0 @@
|
||||
import { LogIn } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { DropdownMenuItem, DropdownMenuSeparator } from 'ui'
|
||||
|
||||
const NoUserDropdown = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-y-2 p-2">
|
||||
<div className="text-sm">
|
||||
Sign in to <span className="text-foreground">database.design</span> to save your database
|
||||
designs!
|
||||
</div>
|
||||
</div>
|
||||
<Link href="/login">
|
||||
<DropdownMenuItem className="space-x-2">
|
||||
<LogIn size={14} className="text-lighter" />
|
||||
<p>Sign in</p>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuSeparator />
|
||||
<a href="https://supabase.com" target="_blank" rel="noreferrer">
|
||||
<DropdownMenuItem className="space-x-2">
|
||||
<Image alt="supabase" src="/supabase.png" width={14} height={14} />
|
||||
<p>Supabase</p>
|
||||
</DropdownMenuItem>
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NoUserDropdown
|
||||
@@ -1,72 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { getAppStateSnapshot } from '@/lib/state'
|
||||
import Image from 'next/image'
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconChevronDown,
|
||||
IconClipboard,
|
||||
IconDownload,
|
||||
} from 'ui'
|
||||
|
||||
const SaveSchemaDropdown = () => {
|
||||
const copyToClipboard = () => {
|
||||
const snap = getAppStateSnapshot()
|
||||
const focused = window.document.hasFocus()
|
||||
if (focused) {
|
||||
window.navigator?.clipboard?.writeText(snap.selectedCode)
|
||||
} else {
|
||||
console.warn('Unable to copy to clipboard')
|
||||
}
|
||||
}
|
||||
|
||||
const downloadSQL = () => {
|
||||
const snap = getAppStateSnapshot()
|
||||
const blob = new Blob([snap.selectedCode], { type: 'text/plain' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.download = 'migration.sql'
|
||||
link.href = url
|
||||
link.click()
|
||||
}
|
||||
|
||||
const loadSQLInSupabase = () => {
|
||||
const snap = getAppStateSnapshot()
|
||||
window.open(
|
||||
`https://supabase.com/dashboard/project/_/sql?content=${encodeURIComponent(
|
||||
snap.selectedCode
|
||||
)}`,
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" iconRight={<IconChevronDown />}>
|
||||
Save schema
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-44" align="end">
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => copyToClipboard()}>
|
||||
<IconClipboard size={14} strokeWidth={1.5} className="text-lighter" />
|
||||
<p>Copy SQL</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => downloadSQL()}>
|
||||
<IconDownload size={14} strokeWidth={1.5} className="text-lighter" />
|
||||
<p>Download SQL</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => loadSQLInSupabase()}>
|
||||
<Image alt="supabase" src="/supabase.png" width={14} height={14} />
|
||||
<p>Load SQL in Supabase</p>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export default SaveSchemaDropdown
|
||||
@@ -1,6 +0,0 @@
|
||||
'use client'
|
||||
import { ThemeToggle } from 'ui-patterns/ThemeToggle'
|
||||
|
||||
export default function ThemeSwitcher() {
|
||||
return <ThemeToggle />
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useAppStateSnapshot } from '@/lib/state'
|
||||
import { Button } from 'ui'
|
||||
|
||||
const ToggleCodeEditorButton = () => {
|
||||
const snap = useAppStateSnapshot()
|
||||
return (
|
||||
<Button type="default" onClick={() => snap.setHideCode(!snap.hideCode)}>
|
||||
{snap.hideCode ? 'Show code' : 'Hide code'}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToggleCodeEditorButton
|
||||
@@ -1,48 +0,0 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { User2 } from 'lucide-react'
|
||||
import { cookies } from 'next/headers'
|
||||
import { Suspense } from 'react'
|
||||
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from 'ui'
|
||||
import AvatarDropdown from './AvatarDropdown'
|
||||
import NoUserDropdown from './NoUserDropdown'
|
||||
|
||||
const EmptyUserButton = () => (
|
||||
<Button type="outline" className="p-1.5 rounded-full" icon={<User2 size={16} />} />
|
||||
)
|
||||
|
||||
export function UserDropdown() {
|
||||
return (
|
||||
<Suspense fallback={<EmptyUserButton />}>
|
||||
<Dropdowns />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
async function Dropdowns() {
|
||||
const supabase = createClient()
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
return (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild className="flex">
|
||||
{user ? (
|
||||
<button
|
||||
title="User dropdown menu"
|
||||
className="border border-foreground-lighter rounded-full w-[30px] h-[30px] bg-no-repeat bg-center bg-cover"
|
||||
style={{ backgroundImage: `url('${user.user_metadata.avatar_url}')` }}
|
||||
/>
|
||||
) : (
|
||||
<EmptyUserButton />
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end" className="w-64">
|
||||
{user ? <AvatarDropdown user={user} /> : <NoUserDropdown />}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserDropdown
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
const GraphLoader = () => {
|
||||
return (
|
||||
<div className="h-full w-full text-muted justify-start border-l">
|
||||
<div className="flex gap-4">
|
||||
<LineNumbers />
|
||||
<div>
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<Loader2 className="animate-spin opacity-100" size={16} strokeWidth={1} />
|
||||
<span className="text-sm">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GraphLoader
|
||||
|
||||
const LineNumbers = () => {
|
||||
const lines = [1, 2, 3, 4, 5, 6]
|
||||
return (
|
||||
<div className="grid gap-2 border-r py-4 px-4 text-sm font-mono text-muted">
|
||||
{lines.map((line) => (
|
||||
<span key={line}>{line}</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
const SchemaLoader = () => {
|
||||
return (
|
||||
<div className="h-full w-full text-muted justify-start">
|
||||
<div className="flex items-center m-4 gap-2">
|
||||
<Loader2 className="animate-spin opacity-100" size={16} strokeWidth={1} />
|
||||
<span className="text-sm">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SchemaLoader
|
||||
@@ -1,20 +0,0 @@
|
||||
export const getTheme = (theme: string) => {
|
||||
const isDarkMode = theme.includes('dark')
|
||||
|
||||
return {
|
||||
base: isDarkMode ? 'vs-dark' : 'vs', // can also be vs-dark or hc-black
|
||||
inherit: true, // can also be false to completely replace the builtin rules
|
||||
rules: [
|
||||
{ token: '', background: isDarkMode ? '1f1f1f' : 'f0f0f0' },
|
||||
{
|
||||
token: '',
|
||||
background: isDarkMode ? '1f1f1f' : 'f0f0f0',
|
||||
foreground: isDarkMode ? 'd4d4d4' : '444444',
|
||||
},
|
||||
{ token: 'string.sql', foreground: '24b47e' },
|
||||
{ token: 'comment', foreground: '666666' },
|
||||
{ token: 'predefined.sql', foreground: isDarkMode ? 'D4D4D4' : '444444' },
|
||||
],
|
||||
colors: { 'editor.background': isDarkMode ? '#1f1f1f' : '#f0f0f0' },
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import Editor, { BeforeMount, EditorProps } from '@monaco-editor/react'
|
||||
import { merge } from 'lodash'
|
||||
|
||||
interface MonacoEditorProps {
|
||||
id: string
|
||||
language: 'pgsql' | 'json' | 'html'
|
||||
autofocus?: boolean
|
||||
defaultValue?: string
|
||||
isReadOnly?: boolean
|
||||
onInputChange?: (value?: string) => void
|
||||
onInputRun?: (value: string) => void
|
||||
hideLineNumbers?: boolean
|
||||
loading?: boolean
|
||||
options?: EditorProps['options']
|
||||
value?: string
|
||||
}
|
||||
|
||||
const MonacoEditor = ({
|
||||
id,
|
||||
language,
|
||||
defaultValue,
|
||||
hideLineNumbers = false,
|
||||
options,
|
||||
value,
|
||||
}: MonacoEditorProps) => {
|
||||
// const monacoRef = useRef<any>()
|
||||
// const { resolvedTheme } = useTheme()
|
||||
|
||||
const beforeMount: BeforeMount = (monaco) => {
|
||||
// monacoRef.current = monaco
|
||||
monaco.editor.defineTheme('supabase', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: '', background: '1f1f1f' },
|
||||
{ token: '', background: '1f1f1f', foreground: 'd4d4d4' },
|
||||
{ token: 'string.sql', foreground: '24b47e' },
|
||||
{ token: 'comment', foreground: '666666' },
|
||||
{ token: 'predefined.sql', foreground: 'D4D4D4' },
|
||||
],
|
||||
colors: { 'editor.background': '#1f1f1f' },
|
||||
})
|
||||
}
|
||||
|
||||
// const onMount: OnMount = async (editor) => {
|
||||
// // Add margin above first line
|
||||
// editor.changeViewZones((accessor) => {
|
||||
// accessor.addZone({
|
||||
// afterLineNumber: 0,
|
||||
// heightInPx: 4,
|
||||
// domNode: document.createElement('div'),
|
||||
// })
|
||||
// })
|
||||
|
||||
// if (resolvedTheme) {
|
||||
// const mode: any = getTheme(resolvedTheme)
|
||||
// monacoRef.current.editor.defineTheme('supabase', mode)
|
||||
// }
|
||||
// }
|
||||
|
||||
const optionsMerged = merge(
|
||||
{
|
||||
tabSize: 2,
|
||||
fontSize: 13,
|
||||
readOnly: true,
|
||||
minimap: { enabled: false },
|
||||
wordWrap: 'on',
|
||||
fixedOverflowWidgets: true,
|
||||
contextmenu: true,
|
||||
lineNumbers: hideLineNumbers ? 'off' : undefined,
|
||||
glyphMargin: hideLineNumbers ? false : undefined,
|
||||
lineNumbersMinChars: hideLineNumbers ? 0 : undefined,
|
||||
occurrencesHighlight: false,
|
||||
folding: hideLineNumbers ? false : undefined,
|
||||
renderLineHighlight: 'none',
|
||||
selectionHighlight: false,
|
||||
},
|
||||
options
|
||||
)
|
||||
|
||||
merge({ cpp: '12' }, { java: '23' }, { python: '35' })
|
||||
|
||||
return (
|
||||
<Editor
|
||||
path={id}
|
||||
theme="supabase"
|
||||
className={'bg-alternative [&>div]:p-0'}
|
||||
value={value ?? undefined}
|
||||
defaultLanguage={language}
|
||||
defaultValue={defaultValue ?? undefined}
|
||||
loading={false}
|
||||
options={optionsMerged}
|
||||
beforeMount={beforeMount}
|
||||
// onMount={onMount}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { MonacoEditor }
|
||||
@@ -1,3 +0,0 @@
|
||||
// ReactFlow is scaling everything by the factor of 2
|
||||
export const NODE_WIDTH = 320
|
||||
export const NODE_ROW_HEIGHT = 40
|
||||
@@ -1,169 +0,0 @@
|
||||
import { PostgresTable } from '@/lib/types'
|
||||
import dagre from '@dagrejs/dagre'
|
||||
import { uniqBy } from 'lodash'
|
||||
import { Edge, Node, Position } from 'reactflow'
|
||||
import { TableNodeData } from 'ui-patterns/SchemaTableNode'
|
||||
|
||||
import { NODE_ROW_HEIGHT, NODE_WIDTH } from './SchemaFlow.constants'
|
||||
|
||||
export async function getGraphDataFromTables(tables: PostgresTable[]): Promise<{
|
||||
nodes: Node<TableNodeData>[]
|
||||
edges: Edge[]
|
||||
}> {
|
||||
if (!tables.length) {
|
||||
return { nodes: [], edges: [] }
|
||||
}
|
||||
|
||||
const nodes = tables.map((table) => {
|
||||
const columns = (table.columns || []).map((column) => {
|
||||
return {
|
||||
id: column.id,
|
||||
isPrimary: table.primary_keys.some((pk) => pk.name === column.name),
|
||||
name: column.name,
|
||||
format: column.format,
|
||||
isNullable: column.is_nullable,
|
||||
isUnique: column.is_unique,
|
||||
isIdentity: column.is_identity,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
id: `${table.id}`,
|
||||
type: 'table',
|
||||
data: {
|
||||
name: table.name,
|
||||
isForeign: false,
|
||||
columns,
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
}
|
||||
})
|
||||
|
||||
const edges: Edge[] = []
|
||||
// const currentSchema = tables[0].schema
|
||||
const uniqueRelationships = uniqBy(
|
||||
tables.flatMap((t) => t.relationships),
|
||||
'id'
|
||||
)
|
||||
|
||||
for (const rel of uniqueRelationships) {
|
||||
// TODO: Support [external->this] relationship?
|
||||
// if (rel.source_schema !== currentSchema) {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// Create additional [this->foreign] node that we can point to on the graph.
|
||||
// if (rel.target_table_schema !== currentSchema) {
|
||||
// nodes.push({
|
||||
// id: rel.constraint_name,
|
||||
// type: 'table',
|
||||
// data: {
|
||||
// name: `${rel.target_table_schema}.${rel.target_table_name}.${rel.target_column_name}`,
|
||||
// isForeign: true,
|
||||
// columns: [],
|
||||
// },
|
||||
// position: { x: 0, y: 0 },
|
||||
// })
|
||||
|
||||
// const [source, sourceHandle] = findTablesHandleIds(
|
||||
// tables,
|
||||
// rel.source_table_name,
|
||||
// rel.source_column_name
|
||||
// )
|
||||
|
||||
// if (source) {
|
||||
// edges.push({
|
||||
// id: String(rel.id),
|
||||
// source,
|
||||
// sourceHandle,
|
||||
// target: rel.constraint_name,
|
||||
// targetHandle: rel.constraint_name,
|
||||
// })
|
||||
// }
|
||||
|
||||
// continue
|
||||
// }
|
||||
|
||||
const [source, sourceHandle] = findTablesHandleIds(
|
||||
tables,
|
||||
rel.source_table_name,
|
||||
rel.source_column_name
|
||||
)
|
||||
const [target, targetHandle] = findTablesHandleIds(
|
||||
tables,
|
||||
rel.target_table_name,
|
||||
rel.target_column_name
|
||||
)
|
||||
|
||||
// We do not support [external->this] flow currently.
|
||||
if (source && target) {
|
||||
edges.push({
|
||||
id: String(rel.id),
|
||||
source,
|
||||
sourceHandle,
|
||||
target,
|
||||
targetHandle,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return getLayoutedElements(nodes, edges)
|
||||
}
|
||||
|
||||
function findTablesHandleIds(
|
||||
tables: PostgresTable[],
|
||||
table_name: string,
|
||||
column_name: string
|
||||
): [string?, string?] {
|
||||
for (const table of tables) {
|
||||
if (table_name !== table.name) continue
|
||||
|
||||
for (const column of table.columns || []) {
|
||||
if (column_name !== column.name) continue
|
||||
|
||||
return [String(table.id), column.id]
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
const getLayoutedElements = (nodes: Node[], edges: Edge[]) => {
|
||||
const dagreGraph = new dagre.graphlib.Graph()
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}))
|
||||
dagreGraph.setGraph({
|
||||
rankdir: 'TB',
|
||||
align: 'UR',
|
||||
nodesep: 25,
|
||||
ranksep: 50,
|
||||
})
|
||||
|
||||
nodes.forEach((node) => {
|
||||
dagreGraph.setNode(node.id, {
|
||||
width: NODE_WIDTH / 2,
|
||||
height: (NODE_ROW_HEIGHT / 2) * (node.data.columns.length + 1), // columns + header
|
||||
})
|
||||
})
|
||||
|
||||
edges.forEach((edge) => {
|
||||
dagreGraph.setEdge(edge.source, edge.target)
|
||||
})
|
||||
|
||||
dagre.layout(dagreGraph)
|
||||
|
||||
nodes.forEach((node) => {
|
||||
const nodeWithPosition = dagreGraph.node(node.id)
|
||||
node.targetPosition = Position.Left
|
||||
node.sourcePosition = Position.Right
|
||||
// We are shifting the dagre node position (anchor=center center) to the top left
|
||||
// so it matches the React Flow node anchor point (top left).
|
||||
node.position = {
|
||||
x: nodeWithPosition.x - nodeWithPosition.width / 2,
|
||||
y: nodeWithPosition.y - nodeWithPosition.height / 2,
|
||||
}
|
||||
|
||||
return node
|
||||
})
|
||||
|
||||
return { nodes, edges }
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
ReactFlowProvider,
|
||||
useReactFlow,
|
||||
} from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
|
||||
import { PostgresTable } from '@/lib/types'
|
||||
import { parseTables } from '@/lib/utils'
|
||||
import { getGraphDataFromTables } from './SchemaFlow.utils'
|
||||
import { TableNode } from 'ui-patterns'
|
||||
|
||||
interface SchemaGraphProps {
|
||||
tables?: PostgresTable[]
|
||||
content: string
|
||||
}
|
||||
|
||||
const SchemaFlowHandler = ({ content }: SchemaGraphProps) => {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [tables, setTables] = useState<PostgresTable[]>([])
|
||||
const reactFlowInstance = useReactFlow()
|
||||
const nodeTypes = useMemo(() => ({ table: TableNode }), [])
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
|
||||
const parseTableData = async () => {
|
||||
try {
|
||||
const tables = await parseTables(content)
|
||||
setTables(tables)
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
}
|
||||
|
||||
parseTableData()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
getGraphDataFromTables(tables).then(({ nodes, edges }) => {
|
||||
reactFlowInstance.setNodes(nodes)
|
||||
reactFlowInstance.setEdges(edges)
|
||||
setTimeout(() => reactFlowInstance.fitView({ minZoom: 1 }), 50)
|
||||
})
|
||||
}, [tables, resolvedTheme, reactFlowInstance])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full grow fade-in">
|
||||
<ReactFlow
|
||||
defaultNodes={[]}
|
||||
defaultEdges={[]}
|
||||
defaultEdgeOptions={{
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
deletable: false,
|
||||
style: {
|
||||
stroke: 'hsl(var(--border-stronger))',
|
||||
strokeWidth: 0.5,
|
||||
},
|
||||
}}
|
||||
nodeTypes={nodeTypes}
|
||||
minZoom={0.8}
|
||||
maxZoom={1.8}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
onInit={(instance) => {
|
||||
instance.fitView()
|
||||
}}
|
||||
>
|
||||
{mounted && (
|
||||
<Background
|
||||
gap={16}
|
||||
className="[&>*]:stroke-foreground-muted opacity-[25%]"
|
||||
variant={BackgroundVariant.Dots}
|
||||
color={'inherit'}
|
||||
/>
|
||||
)}
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ExportedSchemaGraph = ({ content }: SchemaGraphProps) => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<SchemaFlowHandler content={content} />
|
||||
</ReactFlowProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExportedSchemaGraph
|
||||
@@ -1,24 +0,0 @@
|
||||
const ANIMATION_DELAY = 150
|
||||
|
||||
const ShimmeringLoader = ({ className = '', delayIndex = 0, animationDelay = ANIMATION_DELAY }) => {
|
||||
return (
|
||||
<div
|
||||
className={`shimmering-loader rounded py-3 ${className}`}
|
||||
style={{
|
||||
animationFillMode: 'backwards',
|
||||
animationDelay: `${delayIndex * animationDelay}ms`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const GenericSkeletonLoader = () => (
|
||||
<div className="space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
)
|
||||
|
||||
export { GenericSkeletonLoader }
|
||||
export default ShimmeringLoader
|
||||
@@ -1,11 +0,0 @@
|
||||
'use client'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
export function ReactQueryProvider({ children }: PropsWithChildren) {
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
}
|
||||
|
||||
export { ThemeProvider } from 'common'
|
||||
@@ -1,15 +0,0 @@
|
||||
const CHAT_EXAMPLES = [
|
||||
{ label: 'Twitter clone', prompt: 'Create a twitter clone' },
|
||||
{
|
||||
label: 'Chat application',
|
||||
prompt:
|
||||
'Create a chat application that supports sending messages either through channels or directly between users',
|
||||
},
|
||||
{
|
||||
label: 'User management',
|
||||
prompt: 'Create a simple user management schema that supports role based access control',
|
||||
},
|
||||
{ label: 'To do list', prompt: 'Create a simple schema for me to track a to do list' },
|
||||
]
|
||||
|
||||
export { CHAT_EXAMPLES }
|
||||
@@ -1,32 +0,0 @@
|
||||
export type faq = {
|
||||
question: string
|
||||
answer: string
|
||||
}
|
||||
|
||||
export const FAQS = [
|
||||
{
|
||||
question: 'What is database.design?',
|
||||
answer:
|
||||
'database.design is a new way to get ideas out of your head and into code as fast as possible. Supabase provides you tools to making building incredibly fast. This tool focuses on the very first step: turning your idea into a database schema.',
|
||||
},
|
||||
{
|
||||
question: 'How does it work?',
|
||||
answer: `We use OpenAI under the hood to provide responses to your prompts. Just like ChatGPT, you can fine-tune your responses until you're happy with the schema. We're using React Flow to visualize the schemas.`,
|
||||
},
|
||||
{
|
||||
question: 'Can I see the code?',
|
||||
answer: 'You sure can. All of the source code is open source and available on Github. ',
|
||||
},
|
||||
{
|
||||
question: 'Can I contribute to this project?',
|
||||
answer: 'You sure can. All of the source code is open source and available on Github. ',
|
||||
},
|
||||
{
|
||||
question: `What's the license? Can we fork this project and build our own?`,
|
||||
answer: 'Yes. We release software under the Apache License 2.0.',
|
||||
},
|
||||
{
|
||||
question: 'Can I use this for non-Postgres databases?',
|
||||
answer: `Not at the moment. We've tailored the responses to try and do a great job returning Postgres-flavored SQL`,
|
||||
},
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
import { proxy, snapshot, useSnapshot } from 'valtio'
|
||||
|
||||
export const appState = proxy({
|
||||
hideCode: false,
|
||||
setHideCode: (value: boolean) => {
|
||||
appState.hideCode = value
|
||||
},
|
||||
|
||||
selectedCode: '',
|
||||
setSelectedCode: (value: string) => {
|
||||
appState.selectedCode = value
|
||||
},
|
||||
|
||||
loginDialogOpen: false,
|
||||
setLoginDialogOpen: (value: boolean) => {
|
||||
appState.loginDialogOpen = value
|
||||
},
|
||||
|
||||
runsLoading: [] as string[],
|
||||
setRunsLoading: (value: string[]) => {
|
||||
appState.runsLoading = value
|
||||
},
|
||||
})
|
||||
|
||||
export const getAppStateSnapshot = () => snapshot(appState)
|
||||
export const useAppStateSnapshot = (options?: Parameters<typeof useSnapshot>[1]) =>
|
||||
useSnapshot(appState, options)
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Database } from '@/types/supabase'
|
||||
import { createBrowserClient } from '@supabase/ssr'
|
||||
|
||||
export const createClient = () =>
|
||||
createBrowserClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
)
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Database } from '@/types/supabase'
|
||||
import { createServerClient, type CookieOptions } from '@supabase/ssr'
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export const createClient = () => {
|
||||
const cookieStore = cookies()
|
||||
|
||||
return createServerClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get(name: string) {
|
||||
return cookieStore.get(name)?.value
|
||||
},
|
||||
set(name: string, value: string, options: CookieOptions) {
|
||||
try {
|
||||
cookieStore.set({ name, value, ...options })
|
||||
} catch (error) {
|
||||
// The `set` method was called from a Server Component.
|
||||
// This can be ignored if you have middleware refreshing
|
||||
// user sessions.
|
||||
}
|
||||
},
|
||||
remove(name: string, options: CookieOptions) {
|
||||
try {
|
||||
cookieStore.set({ name, value: '', ...options })
|
||||
} catch (error) {
|
||||
// The `delete` method was called from a Server Component.
|
||||
// This can be ignored if you have middleware refreshing
|
||||
// user sessions.
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
export type GeneratedTable = Partial<PostgresTable>
|
||||
|
||||
export type AssistantMessage = {
|
||||
id: string
|
||||
role: 'assistant'
|
||||
created_at: number
|
||||
sql: string
|
||||
json: GeneratedTable[]
|
||||
}
|
||||
|
||||
export type UserMessage = {
|
||||
id: string
|
||||
role: 'user'
|
||||
created_at: number
|
||||
text: string
|
||||
}
|
||||
export type Message = AssistantMessage | UserMessage
|
||||
|
||||
export type ReadThreadAPIResult = {
|
||||
id: string
|
||||
status: 'loading' | 'completed'
|
||||
messages: Message[]
|
||||
}
|
||||
|
||||
export type PostgresColumn = {
|
||||
id: string
|
||||
name: string
|
||||
format: string
|
||||
is_nullable: boolean
|
||||
is_unique: boolean
|
||||
is_identity: boolean
|
||||
}
|
||||
export type PostgresTable = {
|
||||
id: string
|
||||
name: string
|
||||
primary_keys: { name: string }[]
|
||||
relationships: {
|
||||
id: string
|
||||
target_table_name: string
|
||||
target_column_name: string
|
||||
source_table_name: string
|
||||
source_column_name: string
|
||||
}[]
|
||||
columns: PostgresColumn[]
|
||||
}
|
||||
@@ -1,291 +0,0 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { parseQuery } from 'libpg-query'
|
||||
import { compact } from 'lodash'
|
||||
import { z } from 'zod'
|
||||
import { PostgresColumn, PostgresTable } from './types'
|
||||
|
||||
const NameDefinition = z.union([
|
||||
z.object({
|
||||
sval: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
str: z.string(),
|
||||
}),
|
||||
])
|
||||
|
||||
const constraintDefinitionSchema = z.object({
|
||||
Constraint: z.discriminatedUnion('contype', [
|
||||
z
|
||||
.object({
|
||||
contype: z.literal('CONSTR_PRIMARY'),
|
||||
keys: z
|
||||
.array(
|
||||
z.object({
|
||||
String: z.object({
|
||||
sval: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
z.object({
|
||||
contype: z.literal('CONSTR_IDENTITY'),
|
||||
}),
|
||||
z.object({
|
||||
contype: z.literal('CONSTR_NOTNULL'),
|
||||
}),
|
||||
z.object({
|
||||
contype: z.literal('CONSTR_UNIQUE'),
|
||||
}),
|
||||
z.object({
|
||||
contype: z.literal('CONSTR_DEFAULT'),
|
||||
}),
|
||||
z.object({
|
||||
contype: z.literal('CONSTR_FOREIGN'),
|
||||
pktable: z.object({
|
||||
relname: z.string(),
|
||||
}),
|
||||
fk_attrs: z
|
||||
.array(
|
||||
z.object({
|
||||
String: NameDefinition,
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
pk_attrs: z.array(
|
||||
z.object({
|
||||
String: NameDefinition,
|
||||
})
|
||||
),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
|
||||
const columnDefinitionSchema = z.object({
|
||||
ColumnDef: z.object({
|
||||
colname: z.string(),
|
||||
typeName: z.object({
|
||||
names: z.array(z.object({ String: NameDefinition })),
|
||||
}),
|
||||
constraints: z.array(constraintDefinitionSchema).optional(),
|
||||
}),
|
||||
})
|
||||
|
||||
const tableDefinitionSchema = z.object({
|
||||
CreateStmt: z
|
||||
.object({
|
||||
relation: z.object({
|
||||
relname: z.string(),
|
||||
}),
|
||||
tableElts: z.array(z.union([columnDefinitionSchema, constraintDefinitionSchema])),
|
||||
})
|
||||
// the optional() allows other types of statements, but only CreateStmt will be type-safe, we
|
||||
// don't care for the other ones.
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const parseQueryResultSchema = z.object({
|
||||
stmts: z.array(
|
||||
z.object({
|
||||
stmt: tableDefinitionSchema,
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
const extractNameDefinition = (obj: z.infer<typeof NameDefinition>) => {
|
||||
if ('sval' in obj) {
|
||||
return obj?.sval
|
||||
}
|
||||
if ('str' in obj) {
|
||||
return obj?.str
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses SQL into tables compatible with the existing schema visualizer.
|
||||
*
|
||||
* TODO: consider running in WebWorker
|
||||
*/
|
||||
export async function parseTables(sql: string) {
|
||||
// Parse SQL using the real Postgres parser (compiled to WASM)
|
||||
|
||||
const result = await parseQuery(sql)
|
||||
const parsedSql = parseQueryResultSchema.safeParse(result)
|
||||
|
||||
if (!parsedSql.success) {
|
||||
console.log(parsedSql.error)
|
||||
return []
|
||||
}
|
||||
|
||||
// This code generates all columns with their constraints
|
||||
const pgTables: PostgresTable[] = compact(
|
||||
parsedSql.data.stmts.map(({ stmt }) => {
|
||||
if (!stmt.CreateStmt) {
|
||||
return
|
||||
}
|
||||
|
||||
const statement = stmt.CreateStmt
|
||||
|
||||
const columns = compact(
|
||||
statement.tableElts.map((column) => {
|
||||
if ('ColumnDef' in column) {
|
||||
const format = compact(
|
||||
column.ColumnDef.typeName.names
|
||||
.map((name) => extractNameDefinition(name.String))
|
||||
.filter((str) => str !== 'pg_catalog' || !!str)
|
||||
)[0]
|
||||
|
||||
if (!format) {
|
||||
return undefined
|
||||
}
|
||||
const constraints = (column.ColumnDef.constraints || []).map(
|
||||
(c) => c.Constraint.contype
|
||||
)
|
||||
|
||||
const result: PostgresColumn = {
|
||||
name: column.ColumnDef.colname,
|
||||
format: format,
|
||||
id: column.ColumnDef.colname,
|
||||
is_nullable: !constraints.includes('CONSTR_NOTNULL'),
|
||||
is_unique: constraints.includes('CONSTR_UNIQUE'),
|
||||
is_identity: constraints.includes('CONSTR_IDENTITY'),
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// This code processes user_id bigint references users (id) SQL.
|
||||
const columnRelationships = compact(
|
||||
statement.tableElts.map((column) => {
|
||||
if ('ColumnDef' in column) {
|
||||
const found = (column.ColumnDef.constraints || []).find(
|
||||
(c) => c.Constraint.contype === 'CONSTR_FOREIGN'
|
||||
)
|
||||
|
||||
if (found && found.Constraint.contype === 'CONSTR_FOREIGN') {
|
||||
const targetColumn = compact(
|
||||
found.Constraint.pk_attrs.map((name) => extractNameDefinition(name.String))
|
||||
)[0]
|
||||
|
||||
if (!targetColumn) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: `${statement.relation.relname}_${column.ColumnDef.colname}_${found.Constraint.pktable.relname}_${targetColumn}`,
|
||||
source_table_name: statement.relation.relname,
|
||||
source_column_name: column.ColumnDef.colname,
|
||||
target_table_name: found.Constraint.pktable.relname,
|
||||
target_column_name: targetColumn,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// This code processes foreign key (tweet_user_username, tweet_content) references tweets (user_username, content) SQL. It supports composite keys between two tables
|
||||
const tableRelationships = compact(
|
||||
statement.tableElts.flatMap((constraint) => {
|
||||
if ('Constraint' in constraint) {
|
||||
if (
|
||||
constraint.Constraint.contype === 'CONSTR_FOREIGN' &&
|
||||
constraint.Constraint.fk_attrs &&
|
||||
constraint.Constraint.pk_attrs.length === constraint.Constraint.fk_attrs.length
|
||||
) {
|
||||
const pkTable = constraint.Constraint.pktable
|
||||
const pgAttrs = constraint.Constraint.pk_attrs
|
||||
return compact(
|
||||
constraint.Constraint.fk_attrs.map((attr, index) => {
|
||||
const attrString = extractNameDefinition(attr.String)
|
||||
|
||||
const pgAttrString = extractNameDefinition(pgAttrs[index].String)
|
||||
|
||||
if (!attrString || !pgAttrString) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: `${statement.relation.relname}_${attrString}_${pkTable.relname}_${pgAttrString}`,
|
||||
source_table_name: statement.relation.relname,
|
||||
source_column_name: attrString,
|
||||
target_table_name: pkTable.relname,
|
||||
target_column_name: pgAttrString,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const columnPrimaryKeys = compact(
|
||||
statement.tableElts.map((column) => {
|
||||
if ('ColumnDef' in column) {
|
||||
const constraint = (column.ColumnDef.constraints || []).find(
|
||||
(c) => c.Constraint.contype === 'CONSTR_PRIMARY'
|
||||
)
|
||||
if (constraint) {
|
||||
return { name: column.ColumnDef.colname }
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// This code processes primary key (user_username, tweet_user_username, tweet_content) SQL.
|
||||
const tablePrimaryKeys = compact(
|
||||
statement.tableElts.flatMap((constraint) => {
|
||||
if ('Constraint' in constraint) {
|
||||
if (constraint.Constraint.contype === 'CONSTR_PRIMARY' && constraint.Constraint.keys) {
|
||||
return constraint.Constraint.keys.map((key) => ({ name: key.String.sval }))
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
)
|
||||
const table: PostgresTable = {
|
||||
name: statement.relation.relname,
|
||||
columns,
|
||||
id: statement.relation.relname,
|
||||
primary_keys: [...columnPrimaryKeys, ...tablePrimaryKeys],
|
||||
relationships: [...columnRelationships, ...tableRelationships],
|
||||
}
|
||||
|
||||
return table
|
||||
})
|
||||
)
|
||||
|
||||
return pgTables
|
||||
}
|
||||
|
||||
export function timeAgo(date: string) {
|
||||
const createdAt = dayjs(date)
|
||||
const currentTime = dayjs()
|
||||
const timeDifference = currentTime.diff(createdAt, 'seconds') // Time difference in seconds
|
||||
|
||||
if (timeDifference < 60) {
|
||||
// Less than 1 minute
|
||||
return 'just now'
|
||||
} else if (timeDifference < 3600) {
|
||||
// Less than 1 hour
|
||||
return `${Math.floor(timeDifference / 60)} minutes ago`
|
||||
} else if (timeDifference < 86400) {
|
||||
// Less than 1 day (24 hours)
|
||||
return `${Math.floor(timeDifference / 3600)} hours ago`
|
||||
} else {
|
||||
// More than 1 day
|
||||
return createdAt.format('MMM DD, YYYY')
|
||||
}
|
||||
}
|
||||
|
||||
export function slugify(str: string) {
|
||||
return str
|
||||
.toLowerCase() // Convert to lowercase
|
||||
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||
.replace(/[^\w-]+/g, '') // Remove non-word characters except hyphens
|
||||
.replace(/--+/g, '-') // Replace multiple consecutive hyphens with a single hyphen
|
||||
.replace(/^-+|-+$/g, '') // Remove leading and trailing hyphens
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
// Redirect to supabase.com until this site is live
|
||||
// when ready, remove the VERCEL env var at
|
||||
// https://vercel.com/supabase/database-new/settings/environment-variables
|
||||
export function middleware(request: NextRequest) {
|
||||
if (process.env.VERCEL === '1') {
|
||||
return NextResponse.redirect(new URL('https://supabase.com', request.url))
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
transpilePackages: ['ui', 'ui-patterns'],
|
||||
webpack: (config, { dev, isServer, webpack, nextRuntime }) => {
|
||||
config.module.rules.push({
|
||||
test: /\.node$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'nextjs-node-loader',
|
||||
options: {
|
||||
includeWebpackPublicPath: false,
|
||||
outputPath: config.output.path,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
return config
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"name": "database-design",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --port 3002",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dagrejs/dagre": "^1.0.4",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@supabase/ssr": "^0.1.0",
|
||||
"@tanstack/react-query": "^5.13.4",
|
||||
"ai": "^2.2.31",
|
||||
"common": "*",
|
||||
"config": "*",
|
||||
"dayjs": "^1.11.10",
|
||||
"eslint-config-supabase": "*",
|
||||
"libpg-query": "^15.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.338.0",
|
||||
"next": "^14.2.3",
|
||||
"openai": "^4.20.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-flow": "^1.0.3",
|
||||
"sql-formatter": "^13.1.0",
|
||||
"ui": "*",
|
||||
"ui-patterns": "*",
|
||||
"valtio": "^1.12.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.16",
|
||||
"@types/react": "^18.2.24",
|
||||
"@types/react-dom": "^18.2.8",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"nextjs-node-loader": "^1.1.5",
|
||||
"postcss": "^8.4.31",
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large.
Load diff
|
Before Width: | Height: | Size: 461 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||
|
Before Width: | Height: | Size: 629 B |
Vendored
-14
@@ -1,14 +0,0 @@
|
||||
import 'react-dom'
|
||||
|
||||
declare module 'react-dom' {
|
||||
function useFormState<State>(
|
||||
action: (state: State) => Promise<State>,
|
||||
initialState: State,
|
||||
permalink?: string
|
||||
): [state: State, dispatch: () => void]
|
||||
function useFormState<State, Payload>(
|
||||
action: (state: State, payload: Payload) => Promise<State>,
|
||||
initialState: State,
|
||||
permalink?: string
|
||||
): [state: State, dispatch: (payload: Payload) => void]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
# Supabase
|
||||
.branches
|
||||
.temp
|
||||
.env
|
||||
@@ -1,136 +0,0 @@
|
||||
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
||||
# working directory name when running `supabase init`.
|
||||
project_id = "database-design"
|
||||
|
||||
[api]
|
||||
enabled = true
|
||||
# Port to use for the API URL.
|
||||
port = 54321
|
||||
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
|
||||
# endpoints. public and storage are always included.
|
||||
schemas = ["public", "storage", "graphql_public"]
|
||||
# Extra schemas to add to the search_path of every request. public is always included.
|
||||
extra_search_path = ["public", "extensions"]
|
||||
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
|
||||
# for accidental or malicious requests.
|
||||
max_rows = 1000
|
||||
|
||||
[db]
|
||||
# Port to use for the local database URL.
|
||||
port = 54322
|
||||
# Port used by db diff command to initialise the shadow database.
|
||||
shadow_port = 54320
|
||||
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
|
||||
# server_version;` on the remote database to check.
|
||||
major_version = 15
|
||||
|
||||
[db.pooler]
|
||||
enabled = false
|
||||
# Port to use for the local connection pooler.
|
||||
port = 54329
|
||||
# Specifies when a server connection can be reused by other clients.
|
||||
# Configure one of the supported pooler modes: `transaction`, `session`.
|
||||
pool_mode = "transaction"
|
||||
# How many server connections to allow per user/database pair.
|
||||
default_pool_size = 20
|
||||
# Maximum number of client connections allowed.
|
||||
max_client_conn = 100
|
||||
|
||||
[realtime]
|
||||
enabled = true
|
||||
# Bind realtime via either IPv4 or IPv6. (default: IPv6)
|
||||
# ip_version = "IPv6"
|
||||
|
||||
[studio]
|
||||
enabled = true
|
||||
# Port to use for Supabase Studio.
|
||||
port = 54323
|
||||
# External URL of the API server that frontend connects to.
|
||||
api_url = "http://localhost"
|
||||
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
|
||||
openai_api_key = "env(OPENAI_API_KEY)"
|
||||
|
||||
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
|
||||
# are monitored, and you can view the emails that would have been sent from the web interface.
|
||||
[inbucket]
|
||||
enabled = true
|
||||
# Port to use for the email testing server web interface.
|
||||
port = 54324
|
||||
# Uncomment to expose additional ports for testing user applications that send emails.
|
||||
# smtp_port = 54325
|
||||
# pop3_port = 54326
|
||||
|
||||
[storage]
|
||||
enabled = true
|
||||
# The maximum file size allowed (e.g. "5MB", "500KB").
|
||||
file_size_limit = "50MiB"
|
||||
|
||||
[auth]
|
||||
enabled = true
|
||||
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
||||
# in emails.
|
||||
site_url = "http://localhost:3002"
|
||||
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
||||
additional_redirect_urls = ["https://localhost:3002"]
|
||||
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
||||
jwt_expiry = 3600
|
||||
# If disabled, the refresh token will never expire.
|
||||
enable_refresh_token_rotation = true
|
||||
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
|
||||
# Requires enable_refresh_token_rotation = true.
|
||||
refresh_token_reuse_interval = 10
|
||||
# Allow/disallow new user signups to your project.
|
||||
enable_signup = true
|
||||
|
||||
[auth.email]
|
||||
# Allow/disallow new user signups via email to your project.
|
||||
enable_signup = true
|
||||
# If enabled, a user will be required to confirm any email change on both the old, and new email
|
||||
# addresses. If disabled, only the new email is required to confirm.
|
||||
double_confirm_changes = true
|
||||
# If enabled, users need to confirm their email address before signing in.
|
||||
enable_confirmations = false
|
||||
|
||||
# Uncomment to customize email template
|
||||
# [auth.email.template.invite]
|
||||
# subject = "You have been invited"
|
||||
# content_path = "./supabase/templates/invite.html"
|
||||
|
||||
[auth.sms]
|
||||
# Allow/disallow new user signups via SMS to your project.
|
||||
enable_signup = true
|
||||
# If enabled, users need to confirm their phone number before signing in.
|
||||
enable_confirmations = false
|
||||
|
||||
# Use pre-defined map of phone number to OTP for testing.
|
||||
[auth.sms.test_otp]
|
||||
# 4152127777 = "123456"
|
||||
|
||||
# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
|
||||
[auth.sms.twilio]
|
||||
enabled = false
|
||||
account_sid = ""
|
||||
message_service_sid = ""
|
||||
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
|
||||
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
|
||||
|
||||
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
|
||||
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`,
|
||||
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
|
||||
[auth.external.github]
|
||||
enabled = true
|
||||
client_id = "env(GITHUB_CLIENT_ID)"
|
||||
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
|
||||
secret = "env(GITHUB_CLIENT_SECRET)"
|
||||
# Overrides the Github Authorization callback URL. Leave this empty if you've set the url in the Github oath app.
|
||||
redirect_uri = ""
|
||||
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
|
||||
# or any other third-party OIDC providers.
|
||||
url = ""
|
||||
|
||||
[analytics]
|
||||
enabled = false
|
||||
port = 54327
|
||||
vector_port = 54328
|
||||
# Configure one of the supported backends: `postgres`, `bigquery`.
|
||||
backend = "postgres"
|
||||
@@ -1,37 +0,0 @@
|
||||
create table if not exists public.threads (
|
||||
id uuid default gen_random_uuid(),
|
||||
created_at timestamp with time zone not null default now(),
|
||||
user_id uuid null,
|
||||
thread_title text not null,
|
||||
constraint threads_pkey primary key (id),
|
||||
constraint threads_user_id_fkey foreign key (user_id) references auth.users (id) on update cascade on delete cascade
|
||||
);
|
||||
|
||||
create table if not exists public.messages (
|
||||
id uuid default gen_random_uuid(),
|
||||
created_at timestamp with time zone not null default now(),
|
||||
thread_id uuid not null default gen_random_uuid(),
|
||||
message_id uuid not null default gen_random_uuid(),
|
||||
message_role text not null,
|
||||
message_input text not null,
|
||||
message_content text not null,
|
||||
user_id uuid null,
|
||||
constraint messages_pkey primary key (id),
|
||||
constraint messages_thread_id_fkey foreign key (thread_id) references threads (id) on update cascade on delete cascade,
|
||||
constraint messages_user_id_fkey foreign key (user_id) references auth.users (id) on update cascade on delete cascade
|
||||
);
|
||||
|
||||
CREATE
|
||||
OR REPLACE VIEW profile_threads AS
|
||||
SELECT
|
||||
DISTINCT ON (m.thread_id) m.thread_id,
|
||||
m.message_id,
|
||||
m.user_id,
|
||||
m.created_at,
|
||||
t.thread_title
|
||||
FROM
|
||||
public.messages m
|
||||
JOIN public.threads t ON m.thread_id = t.id
|
||||
ORDER BY
|
||||
m.thread_id,
|
||||
m.created_at ASC;
|
||||
Whitespace-only changes.
@@ -1,18 +0,0 @@
|
||||
const config = require('config/tailwind.config')
|
||||
|
||||
export default config({
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'../../packages/ui/src/**/*.{tsx,ts,js}',
|
||||
'../../packages/ui-patterns/**/*.{tsx,ts,js}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
dbnew: '#6046FA',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
// handle ui package paths
|
||||
"@ui/*": ["./../../packages/ui/src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"./../../packages/ui/src/**/*.d.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
|
||||
|
||||
export interface Database {
|
||||
public: {
|
||||
Tables: {
|
||||
messages: {
|
||||
Row: {
|
||||
created_at: string
|
||||
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?: 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?: string
|
||||
message_content?: string
|
||||
message_id?: string
|
||||
message_input?: string
|
||||
message_role?: string
|
||||
thread_id?: string
|
||||
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']
|
||||
},
|
||||
]
|
||||
}
|
||||
threads: {
|
||||
Row: {
|
||||
created_at: string
|
||||
id: string
|
||||
thread_title: string
|
||||
user_id: string | null
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
thread_title: string
|
||||
user_id?: string | null
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
id?: string
|
||||
thread_title?: string
|
||||
user_id?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'threads_user_id_fkey'
|
||||
columns: ['user_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'users'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
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
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
Generated
+2
-132
@@ -28,93 +28,6 @@
|
||||
"npm": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"apps/database-new": {
|
||||
"name": "database-design",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@dagrejs/dagre": "^1.0.4",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@supabase/ssr": "^0.1.0",
|
||||
"@tanstack/react-query": "^5.13.4",
|
||||
"ai": "^2.2.31",
|
||||
"common": "*",
|
||||
"config": "*",
|
||||
"dayjs": "^1.11.10",
|
||||
"eslint-config-supabase": "*",
|
||||
"libpg-query": "^15.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.338.0",
|
||||
"next": "^14.2.3",
|
||||
"openai": "^4.20.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-flow": "^1.0.3",
|
||||
"sql-formatter": "^13.1.0",
|
||||
"ui": "*",
|
||||
"ui-patterns": "*",
|
||||
"valtio": "^1.12.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.16",
|
||||
"@types/react": "^18.2.24",
|
||||
"@types/react-dom": "^18.2.8",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"nextjs-node-loader": "^1.1.5",
|
||||
"postcss": "^8.4.31",
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
},
|
||||
"apps/database-new/node_modules/@tanstack/query-core": {
|
||||
"version": "5.28.8",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.28.8.tgz",
|
||||
"integrity": "sha512-cx64XHeB0kvKxFt22ibvegPeOxnaWVFUbAuhXoIrb7+XePEexHWoB9Kq5n9qroNPkRwQZwgFAP9HNbQz5ohoIg==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"apps/database-new/node_modules/@tanstack/react-query": {
|
||||
"version": "5.28.8",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.28.8.tgz",
|
||||
"integrity": "sha512-4XYhoRmcThqziB32HsyiBLNXJcukaeGfYwAQ+fZqUUE3ZP4oB/Zy41UJdql+TUg98+vsezfbixxAwAbGHfc5Hg==",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.28.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"apps/database-new/node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"apps/database-new/node_modules/lucide-react": {
|
||||
"version": "0.338.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.338.0.tgz",
|
||||
"integrity": "sha512-Uq+vcn/gp6l01GpDH8SxY6eAvO6Ur2bSU39NxEEJt35OotnVCH5q26TZEVPtJf23gTAncXd3DJQqcezIm6HA7w==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"apps/database-new/node_modules/sql-formatter": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-13.1.0.tgz",
|
||||
"integrity": "sha512-/nZQXuN7KzipFNM20ko+dHY4kOr9rymSfZLUDED8rhx3m8OK5y74jcyN+y1L51ZqHqiB0kp40VdpZP99uWvQdA==",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"get-stdin": "=8.0.0",
|
||||
"nearley": "^2.20.1"
|
||||
},
|
||||
"bin": {
|
||||
"sql-formatter": "bin/sql-formatter-cli.cjs"
|
||||
}
|
||||
},
|
||||
"apps/design-system": {
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
@@ -1599,30 +1512,6 @@
|
||||
"tsx": "^4.7.0"
|
||||
}
|
||||
},
|
||||
"apps/storybook": {
|
||||
"version": "1.0.0",
|
||||
"extraneous": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@storybook/node-logger": "^8.0.0-rc.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^8.0.0-rc.1",
|
||||
"@storybook/addon-interactions": "^8.0.0-rc.1",
|
||||
"@storybook/addon-links": "^8.0.0-rc.1",
|
||||
"@storybook/addon-mdx-gfm": "^8.0.0-rc.1",
|
||||
"@storybook/addon-onboarding": "^8.0.0-rc.1",
|
||||
"@storybook/addon-styling-webpack": "^0.0.6",
|
||||
"@storybook/addon-themes": "^8.0.0-rc.1",
|
||||
"@storybook/blocks": "^8.0.0-rc.1",
|
||||
"@storybook/nextjs": "^8.0.0-rc.1",
|
||||
"@storybook/react": "^8.0.0-rc.1",
|
||||
"@storybook/test": "^8.0.0-rc.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"storybook": "^8.0.4"
|
||||
}
|
||||
},
|
||||
"apps/studio": {
|
||||
"version": "0.0.9",
|
||||
"dependencies": {
|
||||
@@ -14521,18 +14410,6 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/ssr": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.1.0.tgz",
|
||||
"integrity": "sha512-bIVrkqjAK5G3KjkIMKYKtAOlCgRRplEWjrlyRyXSOYtgDieiOhk2ZyNAPsEOa1By9OZVxuX5eAW1fitdnuxayw==",
|
||||
"dependencies": {
|
||||
"cookie": "^0.5.0",
|
||||
"ramda": "^0.29.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@supabase/supabase-js": "^2.33.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/storage-js": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.6.0.tgz",
|
||||
@@ -18625,6 +18502,7 @@
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.5.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -19267,10 +19145,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/database-design": {
|
||||
"resolved": "apps/database-new",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.30.0",
|
||||
"license": "MIT",
|
||||
@@ -34500,6 +34374,7 @@
|
||||
},
|
||||
"node_modules/ramda": {
|
||||
"version": "0.29.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -34809,11 +34684,6 @@
|
||||
"react": ">=16.8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/react-flow": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-flow/-/react-flow-1.0.3.tgz",
|
||||
"integrity": "sha512-Rx4Oqqbe9y7NyrUGzCrmtNvuGuMJBVjfIeMFuwbtbMc9f22sUSmxMjIA0c8r6Z7iMtB1zOWoGkTgfpmmpH9ReQ=="
|
||||
},
|
||||
"node_modules/react-from-dom": {
|
||||
"version": "0.6.2",
|
||||
"license": "MIT",
|
||||
|
||||
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user