mirror of
https://github.com/supabase/supabase.git
synced 2026-10-19 16:19:10 +03:00
Merge branch 'master' into auth-users-uid-search
This commit is contained in:
commit
a860d984fb
6717 files changed
+429697
-249826
No files matched your search
+5
-1
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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: ./
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
^./i18n
|
||||
^./i18n
|
||||
^./packages/api-types
|
||||
^./apps/www/lib/redirects.js
|
||||
@@ -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
@@ -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" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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), \
|
||||
@@ -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
@@ -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`:
|
||||
|
||||
@@ -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).
|
||||
|
||||

|
||||

|
||||
|
||||
- [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>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# Update these with your Supabase details from your project settings > API
|
||||
# https://app.supabase.com/project/_/settings/api
|
||||
NEXT_PUBLIC_SUPABASE_URL=your-project-url
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
||||
OPENAI_API_KEY=your-openai-api-key
|
||||
@@ -1,35 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -1,5 +0,0 @@
|
||||
# database.design
|
||||
|
||||
## Overview
|
||||
|
||||
This site will host a tool for designing SQL schemas.
|
||||
@@ -1,20 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useAppStateSnapshot } from '@/lib/state'
|
||||
import { usePathname, useParams } from 'next/navigation'
|
||||
import { LoadingLine as LoadingLineComponent } from 'ui'
|
||||
|
||||
function LoadingLine() {
|
||||
const snap = useAppStateSnapshot()
|
||||
const params = useParams()
|
||||
const pathName = usePathname()
|
||||
|
||||
const runId = params.runId as string
|
||||
|
||||
const loading =
|
||||
runId && snap.runsLoading.includes(runId) && pathName.includes(runId) ? true : false
|
||||
|
||||
return <LoadingLineComponent loading={loading} />
|
||||
}
|
||||
|
||||
export { LoadingLine }
|
||||
@@ -1,19 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useAppStateSnapshot } from '@/lib/state'
|
||||
import { DialogContent_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 }
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import '@ui/layout/ai-icon-animation/ai-icon-animation-style.module.css'
|
||||
import './globals.css'
|
||||
|
||||
import Footer from '@/components/Footer'
|
||||
import Header from '@/components/Header/Header'
|
||||
import { ThemeProvider } from '@/components/providers'
|
||||
import type { Metadata } from 'next'
|
||||
import { LoadingLine } from './LoadingLine'
|
||||
|
||||
const defaultUrl = process.env.VERCEL_URL
|
||||
? `https://${process.env.VERCEL_URL}`
|
||||
: 'http://localhost:3000'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(defaultUrl),
|
||||
title: 'database.design',
|
||||
description: 'Generate schemas from your ideas',
|
||||
}
|
||||
|
||||
// suppressHydrationWarning:
|
||||
// https://github.com/pacocoursey/next-themes#with-app
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" className="dark h-full bg-background" suppressHydrationWarning>
|
||||
<body className="flex flex-col h-full">
|
||||
<ThemeProvider defaultTheme="system" enableSystem disableTransitionOnChange>
|
||||
<Header />
|
||||
<LoadingLine />
|
||||
<main role="main" className="h-full w-full flex flex-col grow">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import LoginForm from '@/components/Auth/LoginForm'
|
||||
|
||||
export default function Login({ searchParams }: { searchParams: { message: string } }) {
|
||||
return <LoginForm searchParams={searchParams} />
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// import LoginForm from '@/components/Auth/LoginForm'
|
||||
import type { Metadata } from 'next'
|
||||
import { LoginDialog } from './LoginDialog'
|
||||
import NewThreadInput from './NewThreadInput'
|
||||
import LoginForm from '@/components/Auth/LoginForm'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'database.design | Create',
|
||||
}
|
||||
|
||||
const NewThread = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { prompt: string; message: string }
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="h-full flex items-center justify-center w-full flex-col gap-y-4">
|
||||
<NewThreadInput />
|
||||
</div>
|
||||
<LoginDialog>
|
||||
<LoginForm searchParams={searchParams} />
|
||||
</LoginDialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewThread
|
||||
@@ -1,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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return <div className="px-4 xl:px-0 xl:max-w-4xl mx-auto h-full">{children}</div>
|
||||
}
|
||||
@@ -1,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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
},
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
export type faq = {
|
||||
question: string
|
||||
answer: string
|
||||
}
|
||||
|
||||
export const FAQS = [
|
||||
{
|
||||
question: 'What is database.design?',
|
||||
answer:
|
||||
'database.design is a new way to get ideas out of your head and into code as fast as possible. Supabase provides you tools to making building incredibly fast. This tool focuses on the very first step: turning your idea into a database schema.',
|
||||
},
|
||||
{
|
||||
question: 'How does it work?',
|
||||
answer: `We use OpenAI under the hood to provide responses to your prompts. Just like ChatGPT, you can fine-tune your responses until you're happy with the schema. We're using React Flow to visualize the schemas.`,
|
||||
},
|
||||
{
|
||||
question: 'Can I see the code?',
|
||||
answer: 'You sure can. All of the source code is open source and available on Github. ',
|
||||
},
|
||||
{
|
||||
question: 'Can I contribute to this project?',
|
||||
answer: 'You sure can. All of the source code is open source and available on Github. ',
|
||||
},
|
||||
{
|
||||
question: `What's the license? Can we fork this project and build our own?`,
|
||||
answer: 'Yes. We release software under the Apache License 2.0.',
|
||||
},
|
||||
{
|
||||
question: 'Can I use this for non-Postgres databases?',
|
||||
answer: `Not at the moment. We've tailored the responses to try and do a great job returning Postgres-flavored SQL`,
|
||||
},
|
||||
]
|
||||
@@ -1,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
|
||||
// },
|
||||
// })
|
||||
@@ -1,27 +0,0 @@
|
||||
import { proxy, snapshot, useSnapshot } from 'valtio'
|
||||
|
||||
export const appState = proxy({
|
||||
hideCode: false,
|
||||
setHideCode: (value: boolean) => {
|
||||
appState.hideCode = value
|
||||
},
|
||||
|
||||
selectedCode: '',
|
||||
setSelectedCode: (value: string) => {
|
||||
appState.selectedCode = value
|
||||
},
|
||||
|
||||
loginDialogOpen: false,
|
||||
setLoginDialogOpen: (value: boolean) => {
|
||||
appState.loginDialogOpen = value
|
||||
},
|
||||
|
||||
runsLoading: [] as string[],
|
||||
setRunsLoading: (value: string[]) => {
|
||||
appState.runsLoading = value
|
||||
},
|
||||
})
|
||||
|
||||
export const getAppStateSnapshot = () => snapshot(appState)
|
||||
export const useAppStateSnapshot = (options?: Parameters<typeof useSnapshot>[1]) =>
|
||||
useSnapshot(appState, options)
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Database } from '@/types/supabase'
|
||||
import { createBrowserClient } from '@supabase/ssr'
|
||||
|
||||
export const createClient = () =>
|
||||
createBrowserClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
)
|
||||
@@ -1,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.
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user