chore: Remove database-design app (#29003)

Remove all files related to database-design app.
This commit is contained in:
Ivan Vasilov authored and GitHub committed 2024-09-02 09:22:42 +00:00
1 parent 24f590e893
commit 5acbd4ab9f
102 files changed
+2 -7962

No files matched your search

-5
View File
@@ -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
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
extends: ['eslint-config-supabase/next'],
}
-35
View File
@@ -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
-5
View File
@@ -1,5 +0,0 @@
# database.design
## Overview
This site will host a tool for designing SQL schemas.
-20
View File
@@ -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 }
-19
View File
@@ -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 }
-34
View File
@@ -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>
)
}
-234
View File
@@ -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)
}
-39
View File
@@ -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

-199
View File
@@ -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;
}
}
-37
View File
@@ -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>
)
}
-5
View File
@@ -1,5 +0,0 @@
import LoginForm from '@/components/Auth/LoginForm'
export default function Login({ searchParams }: { searchParams: { message: string } }) {
return <LoginForm searchParams={searchParams} />
}
-28
View File
@@ -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
-85
View File
@@ -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
-38
View File
@@ -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
-3
View File
@@ -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>
}
-81
View File
@@ -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>
)
}
-24
View File
@@ -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
-24
View File
@@ -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'
-15
View File
@@ -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 }
-32
View File
@@ -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`,
},
]
-27
View File
@@ -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)
-8
View File
@@ -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!
)
-37
View File
@@ -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.
}
},
},
}
)
}
-45
View File
@@ -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[]
}
-291
View File
@@ -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
}
-11
View File
@@ -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))
}
}
-21
View File
@@ -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
-45
View File
@@ -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"
}
}
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
File diff suppressed because it is too large. Load diff

Before

Width:  |  Height:  |  Size: 461 KiB

-1
View File
@@ -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
View File
@@ -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

-14
View File
@@ -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]
}
-4
View File
@@ -1,4 +0,0 @@
# Supabase
.branches
.temp
.env
-136
View File
@@ -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;
View File
Whitespace-only changes.
-18
View File
@@ -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',
},
},
},
})
-35
View File
@@ -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"]
}
-195
View File
@@ -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
+2 -132
View File
@@ -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