Merge branch 'master' into auth-users-uid-search

This commit is contained in:
Alaister Young committed 2024-09-19 16:10:38 +08:00
commit a860d984fb
6717 files changed
+429697 -249826

No files matched your search

+5 -1
View File
@@ -2,5 +2,9 @@
/packages/shared-data/plans.ts @roryw10 @kevcodez
/apps/studio/ @supabase/Dashboard
/apps/www/ @supabase/Website
/apps/www/ @supabase/marketing
/apps/www/public/images/blog @supabase/marketing
/apps/www/lib/redirects.js
/docker/ @supabase/cli
+19
View File
@@ -0,0 +1,19 @@
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file.
YES/NO
## What kind of change does this PR introduce?
Bug fix, feature, docs update, ...
## What is the current behavior?
Please link any relevant issues here.
## What is the new behavior?
Feel free to include screenshots if it includes visual changes.
## Additional context
Add any other context or screenshots.
+4 -8
View File
@@ -22,11 +22,7 @@ jobs:
runs-on: ubuntu-latest
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
strategy:
matrix:
node-version: [18.x]
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
defaults:
run:
@@ -37,10 +33,10 @@ jobs:
with:
sparse-checkout: |
packages
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install deps
run: npm ci
+45
View File
@@ -0,0 +1,45 @@
name: Auto Label Issues
on:
issues:
types: [opened, reopened]
jobs:
check-external:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Check if organization member
id: is-org-member
uses: JamesSingleton/is-organization-member@1.0.0
with:
organization: ${{ github.repository_owner }}
username: ${{ github.event.issue.user.login }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: label-member
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
run: |
if [ ${{ steps.is-org-member.outputs.result }} != "true" ]; then
echo "User is outside of organization, labeling external"
gh issue edit "$NUMBER" --add-label "external-issue"
else
echo "User is within the organization, labeling internal"
gh issue edit "$NUMBER" --add-label "internal-issue"
fi
triage-new:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Label triage
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
LABELS: to-triage
run: |
echo "Applying triage label for new issue"
gh issue edit "$NUMBER" --add-label "$LABELS"
+48
View File
@@ -0,0 +1,48 @@
name: Autofix Linting Checks
on:
pull_request:
types:
- labeled
permissions:
contents: write
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
autofix:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request' && (github.event.label.name == 'autofix') }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: apps
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Install required dependencies
run: npm ci
- name: Run Prettier in fix mode
run: npm run format
- name: Commit changes and push to existing branch
run: |
git config --global user.name 'github-tidy-bot'
git config --global user.email 'github-tidy-bot@supabase.com'
if [[ `git status --porcelain` ]]; then
echo "[bot] Changes detected, committing."
echo "[bot] Running in non-squash mode."
git commit -am "ci: Autofix updates from GitHub workflow"
git push
else
echo "[bot] No changes detected, nothing to commit."
fi
+63
View File
@@ -0,0 +1,63 @@
name: Config (Preview)
on:
pull_request:
types:
- opened
- reopened
- synchronize
branches:
- master
paths:
- "supabase/**"
workflow_dispatch:
jobs:
wait:
runs-on: ubuntu-latest
outputs:
status: ${{ steps.check.outputs.conclusion }}
steps:
- uses: fountainhead/action-wait-for-check@v1.2.0
id: check
with:
checkName: Supabase
ref: ${{ github.event.pull_request.head.sha }}
token: ${{ secrets.GITHUB_TOKEN }}
apply:
needs:
- wait
if: ${{ needs.wait.outputs.status == 'success' }}
runs-on: ubuntu-latest
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
TF_VAR_linked_project: xguihxuzqibwxjnimxev
TF_VAR_git_branch: ${{ github.head_ref }}
TF_CLI_ARGS_apply: -target=supabase_settings.preview
defaults:
run:
working-directory: supabase/remotes
outputs:
db_user: ${{ steps.branch.outputs.user }}
db_pass: ${{ steps.branch.outputs.password }}
db_host: ${{ steps.branch.outputs.host }}
db_port: ${{ steps.branch.outputs.port }}
jwt_secret: ${{ steps.branch.outputs.jwt_secret }}
ref: ${{ steps.branch.outputs.id }}
status: ${{ steps.branch.outputs.status }}
version: ${{ steps.branch.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false
- run: terraform init
- run: terraform apply -auto-approve -no-color
- id: branch
run: |
terraform output -json branch_database \
| jq -r "to_entries|map(\"\(.key)=\(.value|tostring)\")|.[]" \
>> "$GITHUB_OUTPUT"
+28
View File
@@ -0,0 +1,28 @@
name: Config (Production)
on:
push:
branches:
- master
paths:
- "supabase/remotes/**"
workflow_dispatch:
jobs:
apply:
runs-on: ubuntu-latest
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
TF_VAR_linked_project: xguihxuzqibwxjnimxev
defaults:
run:
working-directory: supabase/remotes
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false
- run: terraform init
- run: terraform apply -auto-approve -no-color
+45
View File
@@ -0,0 +1,45 @@
name: '[Docs] Update last-changed dates'
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
inputs:
reset:
description: 'Reset last-updated dates using Git commit dates'
required: false
type: boolean
jobs:
deploy:
runs-on: ubuntu-latest
env:
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SEARCH_SUPABASE_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
steps:
- name: Check out repo
uses: actions/checkout@v4
with:
fetch-depth: 0
sparse-checkout: |
apps/docs
- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Download dependencies
run: npm ci
- name: Update last-changed dates
working-directory: ./apps/docs
if: ${{ !inputs.reset }}
run: npm run last-changed
- name: Reset last-changed dates
working-directory: ./apps/docs
if: ${{ inputs.reset }}
run: npm run last-changed:reset
@@ -0,0 +1,41 @@
name: Update Mgmt Api Docs
on:
schedule:
# Run at 00:00 UTC every Monday
- cron: '0 0 * * 1'
workflow_dispatch:
jobs:
update-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: master
sparse-checkout: |
apps/docs
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install deps
run: npm ci
- name: Change to apps/docs/spec directory and run make command
working-directory: apps/docs/spec
run: make download.api.v1 dereference.api.v1 generate.sections.api.v1 format
- name: Create pull request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'feat: update mgmt api docs'
title: 'feat: update mgmt api docs'
body: 'This PR updates mgmt api docs automatically.'
branch: 'gha/auto-update-mgmt-api-docs'
base: 'master'
+35
View File
@@ -0,0 +1,35 @@
# Smoke tests against the docs production site, run daily
name: Docs Production Smoke Tests
on:
workflow_dispatch:
schedule:
- cron: '0 4 * * *'
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
apps/docs
packages
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install deps
run: npm ci
- name: Run tests
run: npm --prefix="apps/docs" run test:smoke
+3 -7
View File
@@ -15,10 +15,6 @@ jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
steps:
- uses: actions/checkout@v4
with:
@@ -26,10 +22,10 @@ jobs:
apps/docs
packages
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install deps
+25
View File
@@ -0,0 +1,25 @@
name: reviewdog
on:
pull_request:
paths:
- '.vale.ini'
- 'vale/**'
- 'apps/docs/content/**'
jobs:
vale:
name: runner / vale
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
sparse-checkout: |
.vale.ini
vale
apps/docs/content
- uses: errata-ai/vale-action@reviewdog
with:
files: apps/docs/content
# [Charis] Temporary because we still have to go through giant backlog of existing errors
# Don't want people to worry about this (just want them looking at the specific PR comments)
# fail_on_error: true
+45
View File
@@ -0,0 +1,45 @@
name: Playwright Tests
on:
push:
branches: [master]
paths:
- 'apps/studio/**'
- 'package-lock.json'
pull_request:
branches: [master]
paths:
- 'apps/studio/**'
- 'package-lock.json'
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- uses: supabase/setup-cli@v1
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npm run test:playwright
# mark the action as succeeded even if the tests failed. This is temporarily until we make the tests more stable.
# continue-on-error: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-tests/playwright-report/
retention-days: 7
+10 -11
View File
@@ -19,18 +19,16 @@ jobs:
with:
sparse-checkout: apps
- name: Setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version-file: '.nvmrc'
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
- name: Download dependencies
run: |
rm package.json
rm package-lock.json
npm i prettier@2 prettier-plugin-sql-cst
npm ci
- name: Run prettier
run: |-
npx prettier -c 'apps/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'
npm run test:prettier
# i18n is not a node package, so we handle that one separately
format-i18n:
@@ -42,15 +40,15 @@ jobs:
sparse-checkout: |
i18n
- name: Setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version-file: '.nvmrc'
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
- name: Download dependencies
run: |
rm package.json
rm package-lock.json
npm i prettier@2 prettier-plugin-sql-cst
npm i prettier@4.0.0-alpha.8 prettier-plugin-sql-cst
- name: Run prettier
run: |-
npx prettier -c 'i18n/**/*.{js,jsx,ts,tsx,css,md,mdx,json}'
@@ -63,13 +61,14 @@ jobs:
with:
sparse-checkout: |
apps/docs/pages
apps/docs/content
# Installing all dependencies takes up to three minutes, hacking around to only installing prettier+deps
- name: Download dependencies
run: |
rm package.json
rm package-lock.json
npm i prettier@2 prettier-plugin-sql-cst
npm i prettier@4.0.0-alpha.8 prettier-plugin-sql-cst
- name: Run prettier
run: |-
# Check mdx files which contain sql code blocks
grep -lr '```sql' apps/docs/pages/**/*.mdx | xargs npx prettier -c
grep -lr '```sql' apps/docs/{pages,content}/**/*.mdx | xargs npx prettier -c
+5 -11
View File
@@ -1,18 +1,10 @@
name: Publish to Image Registry
on:
push:
tags:
- '*'
# run this action every Monday at 04:00 UTC (Singapore noon)
schedule:
- cron: '0 4 * * 1'
workflow_dispatch:
inputs:
version:
description: 'Image tag'
required: true
type: string
jobs:
settings:
@@ -26,11 +18,10 @@ jobs:
images: |
supabase/studio
flavor: |
latest=false
latest=true
tags: |
type=ref,event=tag
type=sha,prefix={{date 'YYYYMMDD'}}-,enable=${{ github.event_name == 'schedule' }}
type=raw,value=${{ inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }}
type=sha,prefix={{date 'YYYYMMDD'}}-,enable=${{ github.event_name == 'workflow_dispatch' }}
release_x86:
needs: settings
@@ -130,6 +121,9 @@ jobs:
docker buildx imagetools create -t supabase/studio:${{ needs.settings.outputs.image_version }} \
supabase/studio@${{ needs.release_x86.outputs.image_digest }} \
supabase/studio@${{ needs.release_arm.outputs.image_digest }}
docker buildx imagetools create -t supabase/studio:latest \
supabase/studio@${{ needs.release_x86.outputs.image_digest }} \
supabase/studio@${{ needs.release_arm.outputs.image_digest }}
publish:
needs:
+7 -11
View File
@@ -22,12 +22,9 @@ jobs:
runs-on: ubuntu-latest
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
PROJECT_ID: ${{ secrets.SEARCH_SUPABASE_PROJECT_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SEARCH_SUPABASE_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
SUPABASE_DB_PASSWORD: ${{ secrets.SEARCH_SUPABASE_DB_PASSWORD }}
SEARCH_GITHUB_APP_ID: ${{ secrets.SEARCH_GITHUB_APP_ID }}
SEARCH_GITHUB_APP_INSTALLATION_ID: ${{ secrets.SEARCH_GITHUB_APP_INSTALLATION_ID }}
SEARCH_GITHUB_APP_PRIVATE_KEY: ${{ secrets.SEARCH_GITHUB_APP_PRIVATE_KEY }}
@@ -38,21 +35,20 @@ jobs:
with:
sparse-checkout: |
apps/docs
apps/www/.env.local.example
supabase
- name: Setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version-file: '.nvmrc'
- name: Download dependencies
run: npm ci
- name: Link Supabase project
run: npx supabase link --project-ref $PROJECT_ID
- name: Run migrations
run: npx supabase db push
# Need the miscellaneous use API, which is available publicly (by design) in www
- name: Copy environment variables
run: cp apps/www/.env.local.example apps/docs/.env
- name: Update embeddings
working-directory: ./apps/docs
+18
View File
@@ -0,0 +1,18 @@
name: Automatically label stale issues
on:
schedule:
- cron: '30 1 * * *'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Label Stale Issues
uses: actions/stale@v9.0.0
with:
days-before-issue-stale: 30
stale-issue-message: 'After 30 days of inactivity, this issue has been marked as stale. Commenting (or other activity) will remove the stale label.'
stale-issue-label: stale
any-of-labels: contributor
exempt-issue-labels: dnc, needs-analysis
exempt-all-assignees: true
+5 -11
View File
@@ -25,29 +25,23 @@ jobs:
# Uses larger hosted runner as it significantly decreases build times
runs-on: [larger-runner-4cpu]
strategy:
matrix:
node-version: [18.x]
cmd:
- npm run test:studio
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
apps/studio
packages
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install deps
run: npm ci
working-directory: ./
- name: Run ${{ matrix.cmd }}
- name: Run Tests
env:
# Default is 2 GB, increase to have less frequent OOM errors
NODE_OPTIONS: '--max_old_space_size=3072'
run: ${{ matrix.cmd }}
run: npm run test:studio
working-directory: ./
+7 -8
View File
@@ -1,4 +1,4 @@
name: Check code with TypeScript
name: Check code with TypeScript & Lint
on:
pull_request:
@@ -15,18 +15,14 @@ jobs:
# Uses larger hosted runner as it significantly decreases build times
runs-on: [larger-runner-4cpu]
strategy:
matrix:
node-version: [18.x]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install deps
@@ -34,3 +30,6 @@ jobs:
- name: Run TypeScript type check
run: npx turbo run typecheck
- name: Run Lint
run: npx turbo run lint
+34
View File
@@ -0,0 +1,34 @@
name: UI Patterns Tests
on:
pull_request:
branches: [master]
paths:
- 'packages/ui-patterns/**'
# Cancel old builds on new commit for same workflow + branch/PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
packages
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install deps
run: npm ci
- name: Run tests
run: npm run test:ui-patterns
+3 -7
View File
@@ -15,20 +15,16 @@ jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
packages
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install deps
-3
View File
@@ -127,9 +127,6 @@ apps/new-docs/*
# UI tokens
packages/ui/tokens/**/*.json
# UI Storybook build
packages/ui/storybook-static
# For self-hosted logs: https://github.com/supabase/supabase/blob/86e3ab20abfdb9c3e666334d3d2f8efeef9ccf2c/docker/docker-compose-logging.yml#L101
gcloud.json
+3 -1
View File
@@ -1 +1,3 @@
^./i18n
^./i18n
^./packages/api-types
^./apps/www/lib/redirects.js
+1 -1
View File
@@ -1 +1 @@
18
20
+1
View File
@@ -16,3 +16,4 @@ apps/docs/pages/guides/integrations/*.mdx
apps/studio/public
apps/**/.turbo
apps/docs/CONTRIBUTING.md
apps/design-system/__registry__
+7 -1
View File
@@ -7,5 +7,11 @@
"endOfLine": "lf",
"sqlKeywordCase": "lower",
"pluginSearchDirs": false,
"plugins": ["prettier-plugin-sql-cst"]
"plugins": ["prettier-plugin-sql-cst"],
"overrides": [
{
"files": "**/*.json",
"options": { "parser": "json" }
}
]
}
+14
View File
@@ -0,0 +1,14 @@
StylesPath = './vale/styles'
MinAlertLevel = 'suggestion'
Vocab = Supabase
[formats]
mdx = md
[*]
BasedOnStyles = Vale, Custom
BlockIgnores = (?s) *(<StepHikeCompact>.*?</StepHikeCompact>), \
(?s) *(export\s+const\s+\S+\s*=\s*\[.*?\]), \
(?s) *(<Image.*?/>)
TokenIgnores = (href\s*([=:])\s*['"].*?\1), \
+35
View File
@@ -0,0 +1,35 @@
# CONTRIBUTING.md
Thank you for contributing to Supabase! We’re a big, exciting open source project and we’d love to have you contribute! Here’s some resources and guidance to help you get started:
[1. Getting Started](#getting-started)
[2. Issues](#issues)
[3. Pull Requests](#pull-requests)
## Getting Started
To ensure a positive and inclusive environment, please read our [code of conduct](https://github.com/supabase/.github/blob/main/CODE_OF_CONDUCT.md) before contributing. For help setting up the code in this repo, please follow our [DEVELOPERS.md](https://github.com/supabase/supabase/blob/master/DEVELOPERS.md) file. For the [docs](https://supabase.com/docs) site, follow this [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/apps/docs/CONTRIBUTING.md) guide.
## Issues
If you find a bug, please create an Issue and we’ll triage it.
- Please search [existing Issues](https://github.com/supabase/supabase/issues) before creating a new one.
- Please include a clear description of the problem along with steps to reproduce it. Exact steps with screenshots and urls really help here.
## Pull Requests
We actively welcome your Pull Requests! A couple of things to keep in mind before you submit:
- If you’re fixing an Issue, make sure someone else hasn’t already created a PR fixing the same issue. Likewise, make sure to link your PR to the related Issue(s).
- We will always try to accept the first viable PR that resolves the Issue.
- If you're new, we encourage you to take a look at issues tagged with [good first issue](https://github.com/supabase/supabase/labels/good%20first%20issue).
- If you’re submitting a new feature, make sure you have opened a [Discussion](https://github.com/orgs/supabase/discussions/new/choose) to discuss the new feature before opening a PR. We’d love to accept your hard work, but unfortunately if a feature hasn’t gone through a proper design process, your PR will be closed.
- Please use the PR message template and provide detailed context for quicker review. PRs without clear problem statements will be closed.
Prior to submitting your PR, please conduct the following pre-flight checks:
- Run `npm run build` locally to ensure that your code builds successfully without having to wait on us to approve Vercel Preview deploys.
- Ensure that the Prettier tests run successfully on your PR.
Running these before you create the PR will help reduce back and forth with the team.
+14 -5
View File
@@ -1,6 +1,7 @@
# Developing Supabase
- [Developing Supabase](#developing-supabase)
- [Getting started](#getting-started)
- [Install dependencies](#install-dependencies)
- [Local development](#local-development)
@@ -11,8 +12,8 @@
- [Shared components](#shared-components)
- [Installing packages](#installing-packages)
- [Running Docker for Supabase Studio](#running-docker-for-supabase-studio)
- [Prerequsites](#prerequsites)
- [Get Started](#get-started)
- [Prerequsites](#prerequsites)
- [Get Started](#get-started)
- [Create a pull request](#create-a-pull-request)
- [Issue assignment](#issue-assignment)
- [Common tasks](#common-tasks)
@@ -36,8 +37,8 @@ To ensure a positive and inclusive environment, please read our [code of conduct
You will need to install and configure the following dependencies on your machine to build [Supabase](https://supabase.com):
- [Git](http://git-scm.com/)
- [Node.js v18.x (LTS)](http://nodejs.org)
- [npm](https://www.npmjs.com/) version 9.x.x
- [Node.js v20.x (LTS)](http://nodejs.org)
- [npm](https://www.npmjs.com/) version 9.x.x or higher
- [Docker](https://docs.docker.com/get-docker/) (to run studio locally)
## Local development
@@ -71,7 +72,13 @@ To contribute code to [Supabase](https://supabase.com), you must fork the [Supab
npm install # install dependencies
```
2. After that you can run the apps simultaneously with the following.
2. Copy the example `.env.local.example` to `.env.local`
```sh
cp apps/www/.env.local.example apps/www/.env.local
```
3. After that you can run the apps simultaneously with the following.
```sh
npm run dev # start all the applications
```
@@ -92,6 +99,8 @@ You can run any of the sites individually by using the scope name. For example:
npm run dev:www
```
Note: Particularly for `www` make sure you have copied `apps/www/.env.local.example` to `apps/www/.env.local`
#### Shared components
The monorepo has a set of shared components under `/packages`:
+19 -20
View File
@@ -39,7 +39,6 @@ To see how to Contribute, visit [Getting Started](./DEVELOPERS.md)
- [Email Support](https://supabase.com/docs/support#business-support). Best for: problems with your database or infrastructure.
- [Discord](https://discord.supabase.com). Best for: sharing your applications and hanging out with the community.
## How it works
Supabase is a combination of open source tools. We’re building the features of Firebase using enterprise-grade, open source products. If the tools and communities exist, with an MIT, Apache 2, or equivalent open license, we will use and support that tool. If the tool doesn't exist, we build and open source it ourselves. Supabase is not a 1-to-1 mapping of Firebase. Our aim is to give developers a Firebase-like developer experience using open source tools.
@@ -49,7 +48,7 @@ Supabase is a combination of open source tools. We’re building the features of
Supabase is a [hosted platform](https://supabase.com/dashboard). You can sign up and start using Supabase without installing anything.
You can also [self-host](https://supabase.com/docs/guides/hosting/overview) and [develop locally](https://supabase.com/docs/guides/local-development).
![Architecture](https://github.com/supabase/supabase/blob/master/apps/docs/public/img/supabase-architecture.png)
![Architecture](apps/docs/public/img/supabase-architecture.svg)
- [Postgres](https://www.postgresql.org/) is an object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance.
- [Realtime](https://github.com/supabase/realtime) is an Elixir server that allows you to listen to PostgreSQL inserts, updates, and deletes using websockets. Realtime polls Postgres' built-in replication functionality for database changes, converts changes to JSON, then broadcasts the JSON over websockets to authorized clients.
@@ -112,6 +111,24 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
<td><a href="https://github.com/supabase/storage-dart" target="_blank" rel="noopener noreferrer">storage-dart</a></td>
<td><a href="https://github.com/supabase/functions-dart" target="_blank" rel="noopener noreferrer">functions-dart</a></td>
</tr>
<tr>
<td>Swift</td>
<td><a href="https://github.com/supabase/supabase-swift" target="_blank" rel="noopener noreferrer">supabase-swift</a></td>
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/PostgREST" target="_blank" rel="noopener noreferrer">postgrest-swift</a></td>
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Auth" target="_blank" rel="noopener noreferrer">auth-swift</a></td>
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Realtime" target="_blank" rel="noopener noreferrer">realtime-swift</a></td>
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Storage" target="_blank" rel="noopener noreferrer">storage-swift</a></td>
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Functions" target="_blank" rel="noopener noreferrer">functions-swift</a></td>
</tr>
<tr>
<td>Python</td>
<td><a href="https://github.com/supabase/supabase-py" target="_blank" rel="noopener noreferrer">supabase-py</a></td>
<td><a href="https://github.com/supabase/postgrest-py" target="_blank" rel="noopener noreferrer">postgrest-py</a></td>
<td><a href="https://github.com/supabase/gotrue-py" target="_blank" rel="noopener noreferrer">gotrue-py</a></td>
<td><a href="https://github.com/supabase/realtime-py" target="_blank" rel="noopener noreferrer">realtime-py</a></td>
<td><a href="https://github.com/supabase/storage-py" target="_blank" rel="noopener noreferrer">storage-py</a></td>
<td><a href="https://github.com/supabase/functions-py" target="_blank" rel="noopener noreferrer">functions-py</a></td>
</tr>
<!-- /notranslate -->
<th colspan="7">💚 Community 💚</th>
<!-- notranslate -->
@@ -151,15 +168,6 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
<td><a href="https://github.com/supabase-community/supabase-kt/tree/master/Storage" target="_blank" rel="noopener noreferrer">storage-kt</a></td>
<td><a href="https://github.com/supabase-community/supabase-kt/tree/master/Functions" target="_blank" rel="noopener noreferrer">functions-kt</a></td>
</tr>
<tr>
<td>Python</td>
<td><a href="https://github.com/supabase-community/supabase-py" target="_blank" rel="noopener noreferrer">supabase-py</a></td>
<td><a href="https://github.com/supabase-community/postgrest-py" target="_blank" rel="noopener noreferrer">postgrest-py</a></td>
<td><a href="https://github.com/supabase-community/gotrue-py" target="_blank" rel="noopener noreferrer">gotrue-py</a></td>
<td><a href="https://github.com/supabase-community/realtime-py" target="_blank" rel="noopener noreferrer">realtime-py</a></td>
<td><a href="https://github.com/supabase-community/storage-py" target="_blank" rel="noopener noreferrer">storage-py</a></td>
<td><a href="https://github.com/supabase-community/functions-py" target="_blank" rel="noopener noreferrer">functions-py</a></td>
</tr>
<tr>
<td>Ruby</td>
<td><a href="https://github.com/supabase-community/supabase-rb" target="_blank" rel="noopener noreferrer">supabase-rb</a></td>
@@ -178,15 +186,6 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>Swift</td>
<td><a href="https://github.com/supabase-community/supabase-swift" target="_blank" rel="noopener noreferrer">supabase-swift</a></td>
<td><a href="https://github.com/supabase-community/postgrest-swift" target="_blank" rel="noopener noreferrer">postgrest-swift</a></td>
<td><a href="https://github.com/supabase-community/gotrue-swift" target="_blank" rel="noopener noreferrer">gotrue-swift</a></td>
<td><a href="https://github.com/supabase-community/realtime-swift" target="_blank" rel="noopener noreferrer">realtime-swift</a></td>
<td><a href="https://github.com/supabase-community/storage-swift" target="_blank" rel="noopener noreferrer">storage-swift</a></td>
<td><a href="https://github.com/supabase-community/functions-swift" target="_blank" rel="noopener noreferrer">functions-swift</a></td>
</tr>
<tr>
<td>Godot Engine (GDScript)</td>
<td><a href="https://github.com/supabase-community/godot-engine.supabase" target="_blank" rel="noopener noreferrer">supabase-gdscript</a></td>
-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
-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_Shadcn_, Dialog_Shadcn_ } from 'ui'
const LoginDialog = ({ children }: { children: React.ReactNode }) => {
const snap = useAppStateSnapshot()
return (
<Dialog_Shadcn_
open={snap.loginDialogOpen}
onOpenChange={() => snap.setLoginDialogOpen(!snap.loginDialogOpen)}
>
<DialogContent_Shadcn_ className="max-w-md">{children}</DialogContent_Shadcn_>
</Dialog_Shadcn_>
)
}
export { LoginDialog }
-105
View File
@@ -1,105 +0,0 @@
'use client'
import { CHAT_EXAMPLES } from '@/data/chat-examples'
import { useAppStateSnapshot } from '@/lib/state'
import { createClient } from '@/lib/supabase/client'
import { ExternalLink } from 'lucide-react'
import { useState } from 'react'
import { useFormState } from 'react-dom'
import { createThread } from './actions'
import { AssistantChatForm } from '@/components/AssistantChatForm'
import { cn } from 'ui'
const suggestions = CHAT_EXAMPLES
const NewThreadInput = () => {
const [value, setValue] = useState(() => {
if (typeof window !== 'undefined') {
const localPrompt = localStorage.getItem('prompt')
if (localPrompt) {
localStorage.removeItem('prompt')
return localPrompt
}
}
return ''
})
const initialState = {
message: undefined,
success: undefined,
data: {
value,
runId: undefined,
threadId: undefined,
messageId: undefined,
},
}
const [state, formAction] = useFormState(createThread, initialState)
// useEffect(() => {
// if (state.success && state.data.threadId && state.data.runId && state.data.messageId) {
// console.log(state.data)
// const { threadId, runId, messageId } = state.data
// console.log('threadId', threadId)
// console.log('runId', runId)
// console.log('messageId', messageId)
// // router.push(`/${state.data.threadId}/${state.data.runId}/${state.data.messageId}`)
// }
// }, [state.success])
const supabase = createClient()
const snap = useAppStateSnapshot()
return (
<>
<div className="relative w-10/12 xl:w-11/12 max-w-xl">
<AssistantChatForm
action={formAction}
key={'new-thread-form'}
id={'new-thread-form'}
onSubmit={async (event) => {
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) {
event.preventDefault()
localStorage.setItem('prompt', value)
snap.setLoginDialogOpen(true)
return
}
}}
value={value}
placeholder="e.g Create a Telegram-like chat application"
onValueChange={(e) => setValue(e.target.value)}
message={state?.message}
/>
{state?.message && <p>{state?.message}</p>}
</div>
<div className="flex gap-3">
{suggestions.map((suggestion, idx) => (
<button
key={idx}
type="button"
className={cn(
'text-xs',
'flex items-center gap-3 !pr-3',
'transition border rounded-full px-3 py-1.5',
'text-light',
'hover:border-stronger hover:text'
)}
onClick={(event) => {
setValue(suggestion.prompt)
event.preventDefault()
}}
>
{suggestion.label}
<ExternalLink size={12} />
</button>
))}
</div>
</>
)
}
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,36 +0,0 @@
import { Suspense } from 'react'
import { ScrollArea } from 'ui'
import { cn } from '@ui/lib/utils/cn'
import { Messages } from './Messages'
import { ChatInput } from './ChatInput'
import { BottomMarker } from './BottomMarker'
async function Chat({ params }: { params: { threadId: string; runId: string } }) {
return (
<div
className={cn(
'bg',
'h-full',
'border-t xl:border-t-0 xl:border-r relative',
'flex flex-col h-full border-r',
'w-full xl:w-[400px] 2xl:w-[500px]'
)}
>
<div className="flex flex-col grow items-between">
<ScrollArea className="grow h-px">
<div className="flex flex-col py-2 xl:py-6">
<Suspense fallback={<p> loading</p>}>
<Messages params={params} />
</Suspense>
<BottomMarker />
</div>
</ScrollArea>
<ChatInput params={params} />
</div>
</div>
)
}
export { Chat }
@@ -1,63 +0,0 @@
'use client'
import { updateThread } from '@/app/actions'
import { AssistantChatForm } from '@/components/AssistantChatForm'
import { useState } from 'react'
import { useFormState, useFormStatus } from 'react-dom'
import { Input } from 'ui'
function ChatInput({ params }: { params: { threadId: string; runId: string } }) {
const [value, setValue] = useState('')
const { threadId, runId } = params
const initialState = {
message: undefined,
success: undefined,
data: {
value,
},
}
const [state, formAction] = useFormState(updateThread, initialState)
const Message = () => {
const { pending } = useFormStatus()
console.log('state.message', state.message)
return !state.success && state.success !== undefined && !pending ? (
<p className="text-xs text-warning text-center w-full p-3">{state.message}</p>
) : null
}
return (
<div className="px-4 pb-4">
<AssistantChatForm
action={formAction}
key={`chat-thread-form-${runId}`}
id={`chat-thread-form-${runId}`}
value={value}
placeholder={
// loading
// ? 'Generating reply to request...'
// :
'Any changes to make?'
}
onValueChange={(v) => setValue(v.target.value)}
message={state.message}
>
<Input
type="hidden"
name="threadId"
value={threadId}
key={`chat-thread-form-${threadId}`}
/>
<Input type="hidden" name="runId" value={runId} key={`chat-thread-form-${runId}}`} />
</AssistantChatForm>
<Message />
</div>
)
}
export { ChatInput }
@@ -1,56 +0,0 @@
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { sortBy } from 'lodash'
import OpenAI from 'openai'
import UserChat from './UserChat'
dayjs.extend(relativeTime)
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
async function Messages({ params }: { params: { threadId: string; runId: string } }) {
const [run, { data: messages }] = await Promise.all([
openai.beta.threads.runs.retrieve(params.threadId, params.runId),
openai.beta.threads.messages.list(params.threadId),
])
const messagesSorted = sortBy(messages, (m) => m.created_at)
const userMessages = messagesSorted.filter((message) => message.role === 'user')
return (
<div className="flex flex-col py-2 xl:py-6">
{userMessages.map((message, idx) => {
const index = messages.indexOf(message)
const reply = messages[index + 1]
const isLatest = idx === userMessages.length - 1
const hoursFromNow = dayjs().diff(dayjs(message.created_at * 1000), 'hours')
const formattedTimeFromNow = dayjs(message.created_at * 1000).fromNow()
const formattedCreatedAt = dayjs(message.created_at * 1000).format('DD MMM YYYY, HH:mm')
const replyDuration =
reply !== undefined ? reply.created_at - message.created_at : undefined
const times = {
hoursFromNow,
formattedTimeFromNow,
formattedCreatedAt,
replyDuration,
}
return (
<UserChat
key={message.id}
message={message}
run={run}
isLatest={isLatest}
times={times}
/>
)
})}
</div>
)
}
export { Messages }
@@ -1,150 +0,0 @@
'use client'
import { useAppStateSnapshot } from '@/lib/state'
import { pull } from 'lodash'
import Link from 'next/link'
import { useParams, usePathname, useRouter } from 'next/navigation'
import OpenAI from 'openai'
import { useEffect } from 'react'
import { cn } from 'ui'
interface UserChatProps {
message: OpenAI.Beta.Threads.Messages.ThreadMessage
isLatest: boolean
times: {
hoursFromNow: number
formattedTimeFromNow: string
formattedCreatedAt: string
replyDuration: number | undefined
}
run: OpenAI.Beta.Threads.Run
}
const UserChat = ({ message, isLatest, times, run }: UserChatProps) => {
const router = useRouter()
const snap = useAppStateSnapshot()
const { threadId, runId } = useParams()
// console.log(run)
const LOADING_STATUSES = ['in_progress', 'queued']
const runIsInProgressRemotely = LOADING_STATUSES.includes(run.status)
useEffect(() => {
if (runIsInProgressRemotely) {
// set a local state for run loading
// this state will be updated via other client components when completing a run
// remove current message id from array if it exists
let currentRunsLoading = [...snap.runsLoading]
pull(currentRunsLoading, run.id)
const payload = [...currentRunsLoading, run.id]
snap.setRunsLoading([...payload])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [runIsInProgressRemotely, run.id]) // Intentionally left snap out of the dependency array
// using the local state for run loading
const isLoading = snap.runsLoading.includes(run.id) && isLatest
const { hoursFromNow, formattedTimeFromNow, formattedCreatedAt, replyDuration } = times
// chat shown as selected when url matches
const isSelected = usePathname().includes(message.id)
// extract the text from the assistant message
const message_content = message.content[0]
const text = message_content.type === 'text' ? message_content.text.value : ''
return (
<Link
className={cn(
'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={`/${threadId}/${runId}/${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={text}
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'
)}
>
{text}
</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}
{replyDuration !== undefined
? ` with ${replyDuration}s response`
: isLoading
? ', generating response...'
: ''}
</p>
)}
</div>
</Link>
)
}
export default UserChat
@@ -1,50 +0,0 @@
import { format } from 'sql-formatter'
import { getThread } from './MessageId.utils'
import { MonacoEditor } from './MonacoEditor'
import { CodeEditorContainer } from './CodeEditorContainer'
import OpenAI from 'openai'
const openai = new OpenAI()
async function waitForRunCompletion(params: { threadId: string; runId: string }) {
console.log('waiting for run completion')
let realRun = await openai.beta.threads.runs.retrieve(params.threadId, params.runId)
while (realRun.status === 'in_progress') {
//console.log('while..')
await new Promise((resolve) => setTimeout(resolve, 2000))
realRun = await openai.beta.threads.runs.retrieve(params.threadId, params.runId)
//console.log('realRun', realRun.status)
}
//console.log('Run completed:', realRun.status)
}
export async function CodeEditor({ params }: { params: any }) {
const { threadId, runId, messageId } = params
await waitForRunCompletion(params)
const content = await getThread({ threadId, runId, messageId })
const code = format(content, { language: 'postgresql' })
// keep polling until the run is completed
// useEffect(() => {
// snap.setSelectedCode(code)
// }, [code])
/**
* - CodeEditorContainer is a client component, which uses valtio state
* - MonacoEditor is a server component injected into it
*/
return (
<CodeEditorContainer>
<MonacoEditor id="sql-editor" language="pgsql" value={code} />
</CodeEditorContainer>
)
}
@@ -1,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,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,66 +0,0 @@
'use server'
import OpenAI from 'openai'
import { AssistantMessage } from '@/lib/types'
import { compact, sortBy } from 'lodash'
const openai = new OpenAI()
async function getThread({
threadId,
runId,
messageId,
}: {
threadId: string
runId: string
messageId: string
}) {
const [run, { data: _messages }] = await Promise.all([
openai.beta.threads.runs.retrieve(threadId, runId),
openai.beta.threads.messages.list(threadId),
])
let messages = compact(
await Promise.all(
_messages.map(async (m) => {
if (m.role === 'user' && m.content[0].type === 'text') {
return {
id: m.id,
role: 'user' as const,
created_at: m.created_at,
text: m.content[0].text.value,
}
}
if (m.content.length >= 1 && m.content[0].type === 'text') {
let sql = ''
if (m.content[0].type === 'text') {
sql = m.content[0].text.value.replaceAll('\n', '')
}
return {
id: m.id,
role: 'assistant' as const,
created_at: m.created_at,
sql,
}
}
})
)
)
messages = sortBy(messages, (m) => m.created_at)
const userMessages = messages.filter((m) => m.role === 'user')
const selectedMessageIdx = messages.findIndex((m) => m.id === messageId)
const selectedMessageReply = (
selectedMessageIdx !== -1 ? messages[selectedMessageIdx + 1] : undefined
) as AssistantMessage | undefined
const content = selectedMessageReply?.sql.replaceAll('```sql', '').replaceAll('```', '') || ''
return content
}
export { getThread }
@@ -1,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,97 +0,0 @@
import { format } from 'sql-formatter'
import { getThread } from './MessageId.utils'
import { parseTables } from '@/lib/utils'
import SchemaFlowHandler from './SchemaFlowHandler'
import OpenAI from 'openai'
import { createClient } from '@/lib/supabase/server'
import { cookies } from 'next/headers'
const openai = new OpenAI()
async function waitForRunCompletion(params: {
threadId: string
runId: string
messageId: string
}) {
console.log('Waiting for run completion')
let realRun = await openai.beta.threads.runs.retrieve(params.threadId, params.runId)
if (realRun.status !== 'in_progress') {
console.log('Run is not in progress. Exiting without running anything.')
// Check if it exists in Supabase DB
try {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const { data, error } = await supabase
.from('responses_ai')
.select('*')
.eq('message_id', params.messageId)
if (error) throw error
if (data.length > 0) {
return { newMessage: false }
}
} catch (error) {
console.error('The error is', error)
}
}
while (realRun.status === 'in_progress') {
await new Promise((resolve) => setTimeout(resolve, 2000))
realRun = await openai.beta.threads.runs.retrieve(params.threadId, params.runId)
}
console.log('Run completed:', realRun.status)
return { newMessage: true }
}
export async function SchemaFlow({ params }: { params: any }) {
//console.log('schemaFlow refetching')
const { threadId, runId, messageId } = params
// keep polling until the run is completed
const { newMessage } = await waitForRunCompletion(params)
const content = await getThread({ threadId, runId, messageId })
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const {
data: { user },
error: userError,
} = await supabase.auth.getUser()
if (!user) return
if (!newMessage) return
try {
const { error } = await supabase.from('responses_ai').insert({
message_id: messageId,
thread_id: threadId,
text: content,
run_id: runId,
user_id: user.id,
})
if (error) throw error
} catch (error) {
console.error(error)
}
console.log({ content })
//const code = format(content, { language: 'postgresql' })
//const tables = await parseTables(content)
// useEffect(() => {
// snap.setSelectedCode(code)
// }, [code])
return <SchemaFlowHandler content={content} />
}
@@ -1,168 +0,0 @@
import { PostgresTable } from '@/lib/types'
import dagre from '@dagrejs/dagre'
import { uniqBy } from 'lodash'
import { Edge, Node, Position } from 'reactflow'
import { NODE_ROW_HEIGHT, NODE_WIDTH } from './SchemaFlow.constants'
import { TableNodeData } from './TableNode'
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: 'LR',
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,118 +0,0 @@
'use client'
import { PostgresTable } from '@/lib/types'
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 { cn } from 'ui'
import { getGraphDataFromTables } from './SchemaFlow.utils'
import TableNode from './TableNode'
import { useAppStateSnapshot } from '@/lib/state'
import { useParams } from 'next/navigation'
import { set } from 'lodash'
import { parseTables } from '@/lib/utils'
interface SchemaGraphProps {
tables?: PostgresTable[]
content: any
}
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) {
// Handle errors
console.log('error', error)
}
}
parseTableData() // Call the async function
}, [])
useEffect(() => {
getGraphDataFromTables(tables).then(({ nodes, edges }) => {
reactFlowInstance.setNodes(nodes)
reactFlowInstance.setEdges(edges)
setTimeout(() => reactFlowInstance.fitView({}), 10)
})
}, [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) => {
const snap = useAppStateSnapshot()
const params = useParams()
const runId = params.runId as string
useEffect(() => {
const runIsLoading = snap.runsLoading.includes(runId)
if (runIsLoading) {
// let currentRunsLoading = snap.runsLoading
const payload = [...snap.runsLoading.filter((item) => item !== runId)]
snap.setRunsLoading([...payload])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [runId]) // Intentionally left snap out of the dependency array
return (
<ReactFlowProvider>
<SchemaFlowHandler content={content} />
</ReactFlowProvider>
)
}
export default ExportedSchemaGraph
@@ -1,134 +0,0 @@
import { DiamondIcon, Fingerprint, Hash, Key, Table2 } from 'lucide-react'
import { Handle, NodeProps } from 'reactflow'
import { cn } from 'ui'
import { NODE_WIDTH } from './SchemaFlow.constants'
export type TableNodeData = {
name: string
isForeign: boolean
columns: {
id: string
isPrimary: boolean
isNullable: boolean
isUnique: boolean
isIdentity: boolean
name: string
format: string
}[]
}
const TableNode = ({ data, targetPosition, sourcePosition }: NodeProps<TableNodeData>) => {
// Important styles is a nasty hack to use Handles (required for edges calculations), but do not show them in the UI.
// ref: https://github.com/wbkd/react-flow/discussions/2698
const hiddenNodeConnector = '!h-px !w-px !min-w-0 !min-h-0 !cursor-grab !border-0 !opacity-0'
const itemHeight = 'h-[22px]'
return (
<>
{data.isForeign ? (
<div className="rounded-lg">
<header className="text-[0.5rem] leading-5 font-bold px-2 text-center bg-brand text-gray-300">
{data.name}
{targetPosition && (
<Handle
type="target"
id={data.name}
position={targetPosition}
className={cn(hiddenNodeConnector, '!left-0')}
/>
)}
</header>
</div>
) : (
<div
className="border border-[0.5px] overflow-hidden rounded-[4px] shadow-sm"
style={{ width: NODE_WIDTH / 2 }}
>
<header
className={cn(
'text-[0.55rem] px-2 bg-alternative text-default flex gap-1 items-center',
itemHeight
)}
>
<Table2 strokeWidth={1} size={12} className="text-light" />
{data.name}
</header>
{data.columns.map((column) => (
<div
className={cn(
'text-[8px] leading-5 relative flex flex-row justify-items-start',
'bg-surface-100',
'border-t',
'border-t-[0.5px]',
// 'odd:bg-scale-300',
// 'even:bg-scale-400',
'hover:bg-scale-500 transition cursor-default',
itemHeight
)}
key={column.id}
>
<div className="gap-[0.24rem] flex mx-2 align-middle basis-1/5 items-center justify-start">
{column.isPrimary && (
<Key
size={8}
strokeWidth={1}
className={cn(
// 'sb-grid-column-header__inner__primary-key'
'flex-shrink-0',
'text-light'
)}
/>
)}
{column.isNullable && (
<DiamondIcon size={8} strokeWidth={1} className="flex-shrink-0 text-light" />
)}
{!column.isNullable && (
<DiamondIcon
size={8}
strokeWidth={1}
fill="currentColor"
className="flex-shrink-0 text-light"
/>
)}
{column.isUnique && (
<Fingerprint size={8} strokeWidth={1} className="flex-shrink-0 text-light" />
)}
{column.isIdentity && (
<Hash size={8} strokeWidth={1} className="flex-shrink-0 text-light" />
)}
</div>
<div className="flex w-full justify-between">
<span className="text-ellipsis overflow-hidden whitespace-nowrap">
{column.name}
</span>
<span className="px-2 inline-flex justify-end font-mono text-lighter text-[0.4rem]">
{column.format}
</span>
</div>
{targetPosition && (
<Handle
type="target"
id={column.id}
position={targetPosition}
className={cn(hiddenNodeConnector, '!left-0')}
/>
)}
{sourcePosition && (
<Handle
type="source"
id={column.id}
position={sourcePosition}
className={cn(hiddenNodeConnector, '!right-0')}
/>
)}
</div>
))}
</div>
)}
</>
)
}
export default TableNode
@@ -1,29 +0,0 @@
import { CodeEditor } from '@/app/[threadId]/[runId]/[messageId]/CodeEditor'
import { SchemaFlow } from '@/app/[threadId]/[runId]/[messageId]/SchemaFlow'
import GraphLoader from '@/components/Loaders/GraphLoader'
import SchemaLoader from '@/components/Loaders/SchemaLoader'
import { Loader2 } from 'lucide-react'
import Image from 'next/image'
import { Suspense } from 'react'
interface ThreadPageProps {
params: {
threadId: string
runId: string
messageId: string
}
}
export default async function ThreadPage({ params }: ThreadPageProps) {
return (
<div className="grow max-h-screen flex flex-row items-center justify-between bg-alternative h-full">
<Suspense fallback={<SchemaLoader />}>
<SchemaFlow params={params} />
</Suspense>
<Suspense fallback={<GraphLoader />}>
<CodeEditor params={params} />
</Suspense>
</div>
)
}
@@ -1,22 +0,0 @@
import { Chat } from '@/app/[threadId]/[runId]/Chat'
import SaveSchemaDropdown from '@/components/Header/SaveSchemaDropdown'
import ToggleCodeEditorButton from '@/components/Header/ToggleCodeEditorButton'
export default function Layout({
children,
params,
}: {
children: React.ReactNode
params: { threadId: string; runId: string }
}) {
return (
<div className="flex flex-col-reverse items-between xl:flex-row xl:items-center xl:justify-between bg-alternative h-full">
<Chat params={params} />
<div className="xl:hidden flex items-center gap-x-2 justify-end border-t py-2 px-2 bg-background">
<ToggleCodeEditorButton />
<SaveSchemaDropdown />
</div>
{children}
</div>
)
}
@@ -1,74 +0,0 @@
import * as React from 'react'
const RunIdPage = async () => {
return <></>
}
import OpenAI from 'openai'
import { AssistantMessage } from '@/lib/types'
import { compact, sortBy } from 'lodash'
const openai = new OpenAI()
async function getThread({
threadId,
runId,
messageId,
}: {
threadId: string
runId: string
messageId: string
}) {
console.log('getting thread..', threadId, runId, messageId)
const [run, { data: _messages }] = await Promise.all([
openai.beta.threads.runs.retrieve(threadId, runId),
openai.beta.threads.messages.list(threadId),
])
let messages = compact(
await Promise.all(
_messages.map(async (m) => {
if (m.role === 'user' && m.content[0].type === 'text') {
return {
id: m.id,
role: 'user' as const,
created_at: m.created_at,
text: m.content[0].text.value,
}
}
if (m.content.length >= 1 && m.content[0].type === 'text') {
let sql = ''
if (m.content[0].type === 'text') {
sql = m.content[0].text.value.replaceAll('\n', '')
}
return {
id: m.id,
role: 'assistant' as const,
created_at: m.created_at,
sql,
}
}
})
)
)
messages = sortBy(messages, (m) => m.created_at)
// const userMessages = messages.filter((m) => m.role === 'user')
const selectedMessageIdx = messages.findIndex((m) => m.id === messageId)
const selectedMessageReply = (
selectedMessageIdx !== -1 ? messages[selectedMessageIdx + 1] : undefined
) as AssistantMessage | undefined
const content = selectedMessageReply?.sql.replaceAll('```sql', '').replaceAll('```', '') || ''
return content
}
export { getThread }
export default RunIdPage
-293
View File
@@ -1,293 +0,0 @@
'use server'
import { last, sortBy } from 'lodash'
import { AssistantMessage } from '@/lib/types'
import { parseTables } from '@/lib/utils'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
import { cookies } from 'next/headers'
import { RedirectType, redirect } from 'next/navigation'
import OpenAI from 'openai'
import { z } from 'zod'
import { threadId } from 'worker_threads'
import { MessageContentText } from 'openai/resources/beta/threads/index.mjs'
const openai = new OpenAI()
export async function logout() {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const { error } = await supabase.auth.signOut()
if (error) {
console.log('Error logging out:', error.message)
return
}
revalidatePath('/', 'layout')
redirect('/')
}
export async function deleteThread(prevState: any, formData: FormData) {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
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('thread_id', data.thread_id)
await openai.beta.threads.del(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 cookieStore = cookies()
const supabase = createClient(cookieStore)
try {
const schema = z.object({
thread_title: z.string(),
row_id: z.string(),
})
const data = schema.parse({
thread_title: formData.get('thread_title'),
row_id: formData.get('row_id'),
})
const { error } = await supabase
.from('threads')
.update({ thread_title: data.thread_title })
.eq('id', data.row_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,
}
}
}
export async function createThread(prevState: any, formData: FormData) {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
let redirectUrl = ''
try {
const schema = z.object({
value: z.string(),
})
const data = schema.parse({
value: formData.get('value'),
})
const {
data: { user },
error: userError,
} = await supabase.auth.getUser()
if (userError || !user) {
return {
success: false,
message: 'Failed to get user',
data: undefined,
}
}
const thread = await openai.beta.threads.create()
const message = await openai.beta.threads.messages.create(thread.id, {
role: 'user',
content: data.value,
})
const createRun = await openai.beta.threads.runs.create(thread.id, {
assistant_id: 'asst_oLWrK8lScZVNEpfjwUIvBAnq',
})
const [run, { data: messages }] = await Promise.all([
openai.beta.threads.runs.retrieve(thread.id, createRun.id),
openai.beta.threads.messages.list(thread.id),
])
const threadTitle = messages
.filter((m) => m.role === 'user' && m.content[0]?.type === 'text')
.map((m) => {
if (m.content[0]?.type === 'text') {
return m.content[0]?.text?.value
}
return undefined
})
.find((text) => text !== undefined)
try {
const { error } = await supabase.from('threads').insert({
thread_id: thread.id,
run_id: run.id,
user_id: user.id,
thread_title: threadTitle as string,
latest_message_id: message.id,
})
revalidatePath('/profile')
if (error) throw error
} catch (error) {
console.error(error)
}
// insert into supabase
try {
const { error } = await supabase.from('messages_user').insert({
message_id: message.id,
thread_id: thread.id,
text: (message.content[0] as MessageContentText).text.value,
run_id: run.id,
user_id: user.id,
})
if (error) throw error
} catch (error) {
console.error(error)
}
redirectUrl = `/${thread.id}/${run.id}/${message.id}`
// return {
// success: true,
// message: 'Managed to run action',
// data: {
// messageId: message.id,
// runId: run.id,
// threadId: thread.id,
// },
// }
} catch (error: any) {
console.error(error)
return {
success: false,
message: 'Failed to update title to update title',
data: undefined,
}
}
redirect(redirectUrl)
}
export async function updateThread(prevState: any, formData: FormData) {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
let redirectUrl = ''
try {
const schema = z.object({
value: z.string(),
threadId: z.string(),
runId: z.string(),
})
const data = schema.parse({
value: formData.get('value'),
threadId: formData.get('threadId'),
runId: formData.get('runId'),
})
const message = await openai.beta.threads.messages.create(data.threadId, {
content: data.value,
role: 'user',
})
revalidatePath(`/${data.threadId}/${data.runId}`, 'layout')
// insert into supabase
const {
data: { user },
error: userError,
} = await supabase.auth.getUser()
if (userError || !user) {
return {
success: false,
message: 'Failed to get user',
data: undefined,
}
}
console.log('message.content[0]', message.content[0])
try {
const { error } = await supabase.from('messages_user').insert({
message_id: message.id,
thread_id: data.threadId,
text: (message.content[0] as MessageContentText).text.value,
run_id: data.runId,
user_id: user.id,
})
if (error) throw error
} catch (error) {
console.error(error)
}
console.log('message.id', message.id)
// update existing thread with latest messageID
try {
const { error } = await supabase
.from('threads')
.update({
latest_message_id: message.id,
})
.eq('thread_id', data.threadId)
if (error) throw error
} catch (error) {
console.error('Update error:', error)
}
const run = await openai.beta.threads.runs.create(message.thread_id, {
assistant_id: 'asst_oLWrK8lScZVNEpfjwUIvBAnq',
})
revalidatePath(`/${data.threadId}/${data.runId}/${message.id}`, 'layout')
redirectUrl = `/${message.thread_id}/${run.id}/${message.id}`
} catch (error: any) {
return {
success: false,
message: 'Failed to update schema with new prompt',
data: undefined,
}
}
revalidatePath(redirectUrl)
redirect(redirectUrl)
}
@@ -1,13 +0,0 @@
import OpenAI from 'openai'
const openai = new OpenAI()
export async function POST(req: Request, { params }: { params: { threadId: string } }) {
if (!req.body) {
return Response.error()
}
const kill = await openai.beta.threads.del(params.threadId)
console.log({ kill })
return Response.json({})
}
@@ -1,50 +0,0 @@
// import { parseTables } from '@/lib/utils'
import { compact } from 'lodash'
import OpenAI from 'openai'
const openai = new OpenAI()
export async function GET(
req: Request,
{ params }: { params: { threadId: string; runId: string } }
) {
const [run, { data: messages }] = await Promise.all([
openai.beta.threads.runs.retrieve(params.threadId, params.runId),
openai.beta.threads.messages.list(params.threadId),
])
const mappedMessages = compact(
await Promise.all(
messages.map(async (m) => {
if (m.role === 'user' && m.content[0].type === 'text') {
return {
id: m.id,
role: 'user' as const,
created_at: m.created_at,
text: m.content[0].text.value,
}
}
if (m.content.length >= 1 && m.content[0].type === 'text') {
let sql = ''
if (m.content[0].type === 'text') {
sql = m.content[0].text.value.replaceAll('\n', '')
}
return {
id: m.id,
role: 'assistant' as const,
created_at: m.created_at,
sql,
}
}
})
)
)
const result = {
id: params.threadId,
status: run.status === 'completed' ? 'completed' : 'loading',
messages: mappedMessages,
}
return Response.json(result)
}
@@ -1,22 +0,0 @@
import OpenAI from 'openai'
const openai = new OpenAI()
export async function POST(req: Request, { params }: { params: { threadId: string } }) {
if (!req.body) {
return Response.error()
}
const { prompt } = await req.json()
const message = await openai.beta.threads.messages.create(params.threadId, {
content: prompt,
role: 'user',
})
const run = await openai.beta.threads.runs.create(message.thread_id, {
assistant_id: 'asst_oLWrK8lScZVNEpfjwUIvBAnq',
})
return Response.json({ threadId: message.thread_id, runId: run.id })
}
@@ -1,58 +0,0 @@
import { cookies } from 'next/headers'
import { createClient } from '@/lib/supabase/server'
import OpenAI from 'openai'
import { revalidatePath } from 'next/cache'
const openai = new OpenAI()
export async function POST(req: Request) {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
if (!req.body) {
return Response.error()
}
const { prompt, userID } = await req.json()
const thread = await openai.beta.threads.create()
await openai.beta.threads.messages.create(thread.id, {
role: 'user',
content: prompt,
})
const createRun = await openai.beta.threads.runs.create(thread.id, {
assistant_id: 'asst_oLWrK8lScZVNEpfjwUIvBAnq',
})
const [run, { data: messages }] = await Promise.all([
openai.beta.threads.runs.retrieve(thread.id, createRun.id),
openai.beta.threads.messages.list(thread.id),
])
const threadTitle = messages
.filter((m) => m.role === 'user' && m.content[0]?.type === 'text')
.map((m) => {
if (m.content[0]?.type === 'text') {
return m.content[0]?.text?.value
}
return undefined
})
.find((text) => text !== undefined)
try {
const { error } = await supabase.from('threads').insert({
thread_id: thread.id,
run_id: run.id,
user_id: userID,
thread_title: threadTitle as string,
})
if (error) throw error
} catch (error) {
console.error(error)
}
revalidatePath('/profile')
return Response.json({ threadId: thread.id, runId: run.id })
}
@@ -1,20 +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 cookieStore = cookies()
const supabase = createClient(cookieStore)
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
-190
View File
@@ -1,190 +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,82 +0,0 @@
'use client'
import { useEffect, createRef } from 'react'
import { useFormState, useFormStatus } from 'react-dom'
import { Button, Input_Shadcn_, Modal } from 'ui'
import { deleteThread } from '@/app/actions'
import { ThreadType } from './Threads'
const ConfirmDeleteThreadModal = ({
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>
)
}
console.log('thread', thread)
return (
<Modal
size="small"
visible={visible}
onCancel={onClose}
hideFooter
header="Confirm to delete thread?"
className="pb-2"
>
<form action={formAction} key={`${thread.id}-delete-thread-form`}>
<Modal.Content className="py-4">
<p className="text-sm">Once the thread is deleted, it cannot be recovered.</p>
</Modal.Content>
<Modal.Separator />
<Input_Shadcn_ name="thread_id" required type="hidden" value={thread.thread_id} />
<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: {
row_id: 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.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="row_id" value={state.data.row_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
-84
View File
@@ -1,84 +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 formattedTimeAgo = timeAgo(thread.modified_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.run_id}/${thread.latest_message_id}`}
>
<span className="truncate">{thread.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 { Database } from '@/types/supabase'
import EmptyState from './EmptyState'
import Thread from './Thread'
import { cookies } from 'next/headers'
import { createClient } from '@/lib/supabase/server'
export type ThreadType = Database['public']['Tables']['threads']['Row']
async function Threads() {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) return <p>Error fetching user details</p>
const { data } = await supabase
.from('threads')
.select()
.eq('user_id', user.id)
.order('created_at', { ascending: false })
const threads = data ?? []
return (
<div className="flex flex-col gap-y-3">
{threads.length > 0 ? (
threads.sort().map((thread) => <Thread key={`thread-item-${thread.id}`} thread={thread} />)
) : (
<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>
}
-82
View File
@@ -1,82 +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">
<p>Past conversations</p>
<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 cookieStore = cookies()
const supabase = createClient(cookieStore)
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,117 +0,0 @@
import { Loader2 } from 'lucide-react'
import React, { ChangeEvent, createRef, useEffect } from 'react'
import { useFormStatus } from 'react-dom'
import { TextArea_Shadcn_, cn } from 'ui'
export interface FormProps extends React.FormHTMLAttributes<HTMLFormElement> {
loading?: boolean
disabled?: boolean
value?: string
onValueChange: (value: ChangeEvent<HTMLTextAreaElement>) => void
message?: string
children?: React.ReactNode
}
const AssistantChatForm = React.forwardRef<HTMLFormElement, FormProps>(
({ loading, disabled, value, onValueChange, message, ...props }, ref) => {
const textAreaRef = createRef<HTMLTextAreaElement>()
const submitRef = createRef<HTMLButtonElement>()
useEffect(() => {
if (textAreaRef) {
if (!value && textAreaRef && textAreaRef.current) {
textAreaRef.current.style.height = '40px'
} else if (textAreaRef && textAreaRef.current) {
const newHeight = textAreaRef.current.scrollHeight + 'px'
textAreaRef.current.style.height = newHeight
}
}
}, [value, textAreaRef])
useEffect(() => {
textAreaRef?.current?.focus()
}, [value, textAreaRef])
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
// Check if the pressed key is "Enter" (key code 13) without the "Shift" key
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
if (submitRef.current) {
submitRef.current.click()
}
}
}
const SubmitButton = () => {
const { pending } = useFormStatus()
return (
<div className="absolute right-1.5 top-1.5 flex gap-3 items-center">
{loading || pending ? (
<Loader2 size={22} className="animate-spin w-7 h-7 text-muted" strokeWidth={1} />
) : null}
<button
title="Send AI prompt"
ref={submitRef}
type="submit"
disabled={pending}
className={cn(
'transition-all',
'flex items-center justify-center w-7 h-7 border border-control rounded-full mr-0.5 p-1.5 background-alternative',
!value ? 'text-muted opacity-50' : 'text-default opacity-100',
loading || pending ? 'hidden' : ''
)}
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.5 3V2.25H15V3V10C15 10.5523 14.5522 11 14 11H3.56062L5.53029 12.9697L6.06062 13.5L4.99996 14.5607L4.46963 14.0303L1.39641 10.9571C1.00588 10.5666 1.00588 9.93342 1.39641 9.54289L4.46963 6.46967L4.99996 5.93934L6.06062 7L5.53029 7.53033L3.56062 9.5H13.5V3Z"
fill="currentColor"
></path>
</svg>
</button>
</div>
)
}
return (
<form ref={ref} className="relative" {...props}>
<div className={cn('absolute', 'top-2 left-2', 'ml-1 w-6 h-6 rounded-full bg-dbnew')}></div>
<TextArea_Shadcn_
name="value"
ref={textAreaRef}
autoFocus
rows={1}
disabled={disabled || submitRef.current?.disabled}
contentEditable
required
className={
'transition-all text-sm pl-12 pr-10 rounded-[18px] resize-none box-border leading-6'
}
placeholder={props.placeholder}
spellCheck={false}
value={value}
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => onValueChange(event)}
onKeyDown={handleKeyDown}
/>
{props.children}
<SubmitButton />
<p aria-live="polite" className="sr-only" role="status">
{message}
</p>
</form>
)
}
)
AssistantChatForm.displayName = 'AssistantChatForm'
export { AssistantChatForm }
@@ -1,63 +0,0 @@
import { createClient } from '@/lib/supabase/server'
import { Github } from 'lucide-react'
import { cookies, headers } from 'next/headers'
import { redirect } from 'next/navigation'
import { Button } from '@ui/components/Button'
export default function LoginForm({ searchParams }: { searchParams?: { message?: string } }) {
const signUp = async (formData: FormData) => {
'use server'
const origin = headers().get('origin')
const cookieStore = cookies()
const supabase = createClient(cookieStore)
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,28 +0,0 @@
'use client'
import { useParams } from 'next/navigation'
// import { useMessagesQuery } from '@/data/messages-query'
// import { UserMessage } from '@/lib/types'
const CurrentThreadName = () => {
// const { threadId, runId, messageId }: { threadId: string; runId: string; messageId: string } =
// useParams()
// const isConversation = threadId !== undefined && runId !== undefined
// const { data } = useMessagesQuery({ threadId, runId, enabled: isConversation })
// const selectedMessage = data?.messages.find((m) => m.id === messageId) as UserMessage
return (
// <div className="hidden xl:block flex items-center gap-x-4">
// {selectedMessage !== undefined && (
// <p title={selectedMessage.text} className="truncate max-w-[700px] border-l text-sm px-4">
// {selectedMessage.text}
// </p>
// )}
// </div>
<></>
)
}
export default CurrentThreadName
@@ -1,40 +0,0 @@
import HeaderActions from './HeaderActions'
import { createClient } from '@/lib/supabase/server'
import { cookies } from 'next/headers'
import CurrentThreadName from './CurrentThreadName'
import Link from 'next/link'
import UserDropdown from './UserDropdown'
const Header = async () => {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const {
data: { user },
} = await supabase.auth.getUser()
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>
<CurrentThreadName />
</div>
<div className="flex gap-3">
<HeaderActions user={user} />
<UserDropdown />
</div>
</nav>
)
}
export default Header
@@ -1,59 +0,0 @@
'use client'
import { useAppStateSnapshot } from '@/lib/state'
import { createClient } from '@/lib/supabase/client'
import { User } from '@supabase/supabase-js'
import Link from 'next/link'
import { useRouter, useSelectedLayoutSegment } from 'next/navigation'
import { useState } from 'react'
import { Button } from 'ui'
import AvatarDropdown from './AvatarDropdown'
import NoUserDropdown from './NoUserDropdown'
import SaveSchemaDropdown from './SaveSchemaDropdown'
import ThemeSwitcherButton from './ThemeSwitcher'
import ToggleCodeEditorButton from './ToggleCodeEditorButton'
interface HeaderActionsProps {
user: User | null
}
const HeaderActions = ({ user }: HeaderActionsProps) => {
const supabase = createClient()
const router = useRouter()
const segment = useSelectedLayoutSegment()
const [currentUser, setCurrentUser] = useState<User | null>(user)
async function signout() {
const { error } = await supabase.auth.signOut()
if (error) {
console.log('Error logging out:', error.message)
return
}
router.push('/')
setCurrentUser(null)
}
return (
<div className="flex items-center gap-x-2">
{segment && segment.includes('thread') && (
<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>
{/* {currentUser ? (
<AvatarDropdown currentUser={currentUser} signout={signout} />
) : (
<NoUserDropdown />
)} */}
</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,74 +0,0 @@
'use client'
import { getAppStateSnapshot, useAppStateSnapshot } from '@/lib/state'
import Image from 'next/image'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconChevronDown,
IconClipboard,
IconDownload,
} from 'ui'
const SaveSchemaDropdown = () => {
const snap = useAppStateSnapshot()
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/components/ThemeProvider/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,49 +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 cookieStore = cookies()
const supabase = createClient(cookieStore)
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,32 +0,0 @@
import { Loader2 } from 'lucide-react'
// output numbers from 1-6 in a span tag
// const numbers = Array.from({ length: 6 }, (_, i) => i + 1)
const GraphLoader = async () => {
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 = async () => {
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
-24
View File
@@ -1,24 +0,0 @@
const ANIMATION_DELAY = 150
const ShimmeringLoader = ({ className = '', delayIndex = 0, animationDelay = 150 }) => {
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 }
@@ -1,52 +0,0 @@
import { useQuery } from '@tanstack/react-query'
export type ConversationsVariables = {
userId: string
enabled?: boolean
}
// Just update based on the schema - or use the dashboard's type generation feature 😉
export type Conversation = {
id: string
name: string
threadId: string
runId: string
createdAt: string
updatedAt: string
}
export const useConversationsQuery = ({ userId, enabled }: ConversationsVariables) =>
useQuery<Conversation[]>({
queryKey: [userId, 'conversations'],
enabled: enabled && !!userId,
queryFn: async () => {
// [Joshen] Just mocking the conversations data, to replace with fetching from Supabase
const getConversations = (): Promise<Conversation[]> => {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve([
{
id: '1',
name: 'Twitter clone',
threadId: 'thread_vrXIl16bUusqFMUFYDw8CoEy',
runId: 'run_4DiPb5ppb5hwY1y6dT1qEWzc',
createdAt: '2022-07-29 07:53:58.560926+00',
updatedAt: '2022-07-29 07:53:58.560926+00',
},
{
id: '2',
name: 'Supabase clone',
threadId: 'thread_vrXIl16bUusqFMUFYDw8CoEy',
runId: 'run_4DiPb5ppb5hwY1y6dT1qEWzc',
createdAt: '2022-07-29 07:53:58.560926+00',
updatedAt: '2022-07-30 07:53:58.560926+00',
},
])
}, 100)
})
}
const result = await getConversations()
return result
},
})
-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`,
},
]
-28
View File
@@ -1,28 +0,0 @@
// import { useQuery } from '@tanstack/react-query'
// import { ReadThreadAPIResult } from '@/lib/types'
// export type MessagesVariables = {
// threadId: string
// runId: string
// enabled?: boolean
// }
// export const useMessagesQuery = ({ threadId, runId, enabled }: MessagesVariables) =>
// useQuery<ReadThreadAPIResult>({
// queryKey: [threadId, runId],
// enabled: enabled && !!(threadId && runId),
// queryFn: async () => {
// const response = await fetch(`/api/ai/sql/threads/${threadId}/read/${runId}`, {
// method: 'GET',
// })
// const result = await response.json()
// return result
// },
// refetchInterval: (options) => {
// const data = options.state.data
// if (data && data.status === 'completed') {
// return Infinity
// } else return 5000
// },
// })
-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!
)
-35
View File
@@ -1,35 +0,0 @@
import { Database } from '@/types/supabase'
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export const createClient = (cookieStore: ReturnType<typeof 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[]
}
Loaded 100 of 6717 files, more files were not shown because too many files have changed in this diff. Show more