chore: use prettier for project and apply to all files

This commit is contained in:
Tianzhou Chen
2023-04-29 22:49:07 +08:00
parent 74754e1476
commit ea7a48a26b
55 changed files with 1040 additions and 434 deletions

36
.vscode/settings.json vendored
View File

@ -1,5 +1,33 @@
{
"i18n-ally.localesPaths": [
"src/locales"
]
}
"i18n-ally.localesPaths": ["src/locales"],
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file"
},
"[yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file"
}
}

View File

@ -64,6 +64,7 @@
"mysql2": "^3.2.0",
"pg": "^8.10.0",
"postcss": "^8.4.20",
"prettier": "^2.8.8",
"prisma": "^4.13.0",
"react-syntax-highlighter": "^15.5.0",
"tailwindcss": "^3.2.4",

345
pnpm-lock.yaml generated
View File

@ -159,6 +159,9 @@ devDependencies:
postcss:
specifier: ^8.4.20
version: 8.4.21
prettier:
specifier: ^2.8.8
version: 2.8.8
prisma:
specifier: ^4.13.0
version: 4.13.0
@ -883,6 +886,123 @@ packages:
glob: 7.1.7
dev: true
/@next/swc-android-arm-eabi@13.2.4:
resolution: {integrity: sha512-DWlalTSkLjDU11MY11jg17O1gGQzpRccM9Oes2yTqj2DpHndajrXHGxj9HGtJ+idq2k7ImUdJVWS2h2l/EDJOw==}
engines: {node: '>= 10'}
cpu: [arm]
os: [android]
requiresBuild: true
dev: false
optional: true
/@next/swc-android-arm64@13.2.4:
resolution: {integrity: sha512-sRavmUImUCf332Gy+PjIfLkMhiRX1Ez4SI+3vFDRs1N5eXp+uNzjFUK/oLMMOzk6KFSkbiK/3Wt8+dHQR/flNg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
requiresBuild: true
dev: false
optional: true
/@next/swc-darwin-arm64@13.2.4:
resolution: {integrity: sha512-S6vBl+OrInP47TM3LlYx65betocKUUlTZDDKzTiRDbsRESeyIkBtZ6Qi5uT2zQs4imqllJznVjFd1bXLx3Aa6A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@next/swc-darwin-x64@13.2.4:
resolution: {integrity: sha512-a6LBuoYGcFOPGd4o8TPo7wmv5FnMr+Prz+vYHopEDuhDoMSHOnC+v+Ab4D7F0NMZkvQjEJQdJS3rqgFhlZmKlw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@next/swc-freebsd-x64@13.2.4:
resolution: {integrity: sha512-kkbzKVZGPaXRBPisoAQkh3xh22r+TD+5HwoC5bOkALraJ0dsOQgSMAvzMXKsN3tMzJUPS0tjtRf1cTzrQ0I5vQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [freebsd]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-arm-gnueabihf@13.2.4:
resolution: {integrity: sha512-7qA1++UY0fjprqtjBZaOA6cas/7GekpjVsZn/0uHvquuITFCdKGFCsKNBx3S0Rpxmx6WYo0GcmhNRM9ru08BGg==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-arm64-gnu@13.2.4:
resolution: {integrity: sha512-xzYZdAeq883MwXgcwc72hqo/F/dwUxCukpDOkx/j1HTq/J0wJthMGjinN9wH5bPR98Mfeh1MZJ91WWPnZOedOg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-arm64-musl@13.2.4:
resolution: {integrity: sha512-8rXr3WfmqSiYkb71qzuDP6I6R2T2tpkmf83elDN8z783N9nvTJf2E7eLx86wu2OJCi4T05nuxCsh4IOU3LQ5xw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-x64-gnu@13.2.4:
resolution: {integrity: sha512-Ngxh51zGSlYJ4EfpKG4LI6WfquulNdtmHg1yuOYlaAr33KyPJp4HeN/tivBnAHcZkoNy0hh/SbwDyCnz5PFJQQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-x64-musl@13.2.4:
resolution: {integrity: sha512-gOvwIYoSxd+j14LOcvJr+ekd9fwYT1RyMAHOp7znA10+l40wkFiMONPLWiZuHxfRk+Dy7YdNdDh3ImumvL6VwA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-win32-arm64-msvc@13.2.4:
resolution: {integrity: sha512-q3NJzcfClgBm4HvdcnoEncmztxrA5GXqKeiZ/hADvC56pwNALt3ngDC6t6qr1YW9V/EPDxCYeaX4zYxHciW4Dw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@next/swc-win32-ia32-msvc@13.2.4:
resolution: {integrity: sha512-/eZ5ncmHUYtD2fc6EUmAIZlAJnVT2YmxDsKs1Ourx0ttTtvtma/WKlMV5NoUsyOez0f9ExLyOpeCoz5aj+MPXw==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@next/swc-win32-x64-msvc@13.2.4:
resolution: {integrity: sha512-0MffFmyv7tBLlji01qc0IaPP/LVExzvj7/R5x1Jph1bTAIj4Vu81yFQWHHQAP6r4ff9Ukj1mBK6MDNVXm7Tcvw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@nodelib/fs.scandir@2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -1966,7 +2086,7 @@ packages:
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: registry.npmmirror.com/fsevents@2.3.2
fsevents: 2.3.2
dev: true
/client-only@0.0.1:
@ -2793,6 +2913,14 @@ packages:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
dev: true
/fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
dev: true
optional: true
/function-bind@1.1.1:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
@ -4016,19 +4144,19 @@ packages:
react-dom: 18.2.0(react@18.2.0)
styled-jsx: 5.1.1(react@18.2.0)
optionalDependencies:
'@next/swc-android-arm-eabi': registry.npmmirror.com/@next/swc-android-arm-eabi@13.2.4
'@next/swc-android-arm64': registry.npmmirror.com/@next/swc-android-arm64@13.2.4
'@next/swc-darwin-arm64': registry.npmmirror.com/@next/swc-darwin-arm64@13.2.4
'@next/swc-darwin-x64': registry.npmmirror.com/@next/swc-darwin-x64@13.2.4
'@next/swc-freebsd-x64': registry.npmmirror.com/@next/swc-freebsd-x64@13.2.4
'@next/swc-linux-arm-gnueabihf': registry.npmmirror.com/@next/swc-linux-arm-gnueabihf@13.2.4
'@next/swc-linux-arm64-gnu': registry.npmmirror.com/@next/swc-linux-arm64-gnu@13.2.4
'@next/swc-linux-arm64-musl': registry.npmmirror.com/@next/swc-linux-arm64-musl@13.2.4
'@next/swc-linux-x64-gnu': registry.npmmirror.com/@next/swc-linux-x64-gnu@13.2.4
'@next/swc-linux-x64-musl': registry.npmmirror.com/@next/swc-linux-x64-musl@13.2.4
'@next/swc-win32-arm64-msvc': registry.npmmirror.com/@next/swc-win32-arm64-msvc@13.2.4
'@next/swc-win32-ia32-msvc': registry.npmmirror.com/@next/swc-win32-ia32-msvc@13.2.4
'@next/swc-win32-x64-msvc': registry.npmmirror.com/@next/swc-win32-x64-msvc@13.2.4
'@next/swc-android-arm-eabi': 13.2.4
'@next/swc-android-arm64': 13.2.4
'@next/swc-darwin-arm64': 13.2.4
'@next/swc-darwin-x64': 13.2.4
'@next/swc-freebsd-x64': 13.2.4
'@next/swc-linux-arm-gnueabihf': 13.2.4
'@next/swc-linux-arm64-gnu': 13.2.4
'@next/swc-linux-arm64-musl': 13.2.4
'@next/swc-linux-x64-gnu': 13.2.4
'@next/swc-linux-x64-musl': 13.2.4
'@next/swc-win32-arm64-msvc': 13.2.4
'@next/swc-win32-ia32-msvc': 13.2.4
'@next/swc-win32-x64-msvc': 13.2.4
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
@ -4379,6 +4507,12 @@ packages:
engines: {node: '>= 0.8.0'}
dev: true
/prettier@2.8.8:
resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
engines: {node: '>=10.13.0'}
hasBin: true
dev: true
/prisma@4.13.0:
resolution: {integrity: sha512-L9mqjnSmvWIRCYJ9mQkwCtj4+JDYYTdhoyo8hlsHNDXaZLh/b4hR0IoKIBbTKxZuyHQzLopb/+0Rvb69uGV7uA==}
engines: {node: '>=14.17'}
@ -5574,7 +5708,7 @@ packages:
dev: false
registry.npmmirror.com/@babel/runtime@7.21.0:
resolution: {integrity: sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@babel/runtime/-/runtime-7.21.0.tgz}
resolution: {integrity: sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==, registry: https://registry.npmjs.org/, tarball: https://registry.npmmirror.com/@babel/runtime/-/runtime-7.21.0.tgz}
name: '@babel/runtime'
version: 7.21.0
engines: {node: '>=6.9.0'}
@ -5582,155 +5716,8 @@ packages:
regenerator-runtime: registry.npmmirror.com/regenerator-runtime@0.13.11
dev: false
registry.npmmirror.com/@next/swc-android-arm-eabi@13.2.4:
resolution: {integrity: sha512-DWlalTSkLjDU11MY11jg17O1gGQzpRccM9Oes2yTqj2DpHndajrXHGxj9HGtJ+idq2k7ImUdJVWS2h2l/EDJOw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.2.4.tgz}
name: '@next/swc-android-arm-eabi'
version: 13.2.4
engines: {node: '>= 10'}
cpu: [arm]
os: [android]
requiresBuild: true
dev: false
optional: true
registry.npmmirror.com/@next/swc-android-arm64@13.2.4:
resolution: {integrity: sha512-sRavmUImUCf332Gy+PjIfLkMhiRX1Ez4SI+3vFDRs1N5eXp+uNzjFUK/oLMMOzk6KFSkbiK/3Wt8+dHQR/flNg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@next/swc-android-arm64/-/swc-android-arm64-13.2.4.tgz}
name: '@next/swc-android-arm64'
version: 13.2.4
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
requiresBuild: true
dev: false
optional: true
registry.npmmirror.com/@next/swc-darwin-arm64@13.2.4:
resolution: {integrity: sha512-S6vBl+OrInP47TM3LlYx65betocKUUlTZDDKzTiRDbsRESeyIkBtZ6Qi5uT2zQs4imqllJznVjFd1bXLx3Aa6A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.2.4.tgz}
name: '@next/swc-darwin-arm64'
version: 13.2.4
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
registry.npmmirror.com/@next/swc-darwin-x64@13.2.4:
resolution: {integrity: sha512-a6LBuoYGcFOPGd4o8TPo7wmv5FnMr+Prz+vYHopEDuhDoMSHOnC+v+Ab4D7F0NMZkvQjEJQdJS3rqgFhlZmKlw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.2.4.tgz}
name: '@next/swc-darwin-x64'
version: 13.2.4
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
registry.npmmirror.com/@next/swc-freebsd-x64@13.2.4:
resolution: {integrity: sha512-kkbzKVZGPaXRBPisoAQkh3xh22r+TD+5HwoC5bOkALraJ0dsOQgSMAvzMXKsN3tMzJUPS0tjtRf1cTzrQ0I5vQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.2.4.tgz}
name: '@next/swc-freebsd-x64'
version: 13.2.4
engines: {node: '>= 10'}
cpu: [x64]
os: [freebsd]
requiresBuild: true
dev: false
optional: true
registry.npmmirror.com/@next/swc-linux-arm-gnueabihf@13.2.4:
resolution: {integrity: sha512-7qA1++UY0fjprqtjBZaOA6cas/7GekpjVsZn/0uHvquuITFCdKGFCsKNBx3S0Rpxmx6WYo0GcmhNRM9ru08BGg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.2.4.tgz}
name: '@next/swc-linux-arm-gnueabihf'
version: 13.2.4
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: false
optional: true
registry.npmmirror.com/@next/swc-linux-arm64-gnu@13.2.4:
resolution: {integrity: sha512-xzYZdAeq883MwXgcwc72hqo/F/dwUxCukpDOkx/j1HTq/J0wJthMGjinN9wH5bPR98Mfeh1MZJ91WWPnZOedOg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.2.4.tgz}
name: '@next/swc-linux-arm64-gnu'
version: 13.2.4
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
requiresBuild: true
dev: false
optional: true
registry.npmmirror.com/@next/swc-linux-arm64-musl@13.2.4:
resolution: {integrity: sha512-8rXr3WfmqSiYkb71qzuDP6I6R2T2tpkmf83elDN8z783N9nvTJf2E7eLx86wu2OJCi4T05nuxCsh4IOU3LQ5xw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.2.4.tgz}
name: '@next/swc-linux-arm64-musl'
version: 13.2.4
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
requiresBuild: true
dev: false
optional: true
registry.npmmirror.com/@next/swc-linux-x64-gnu@13.2.4:
resolution: {integrity: sha512-Ngxh51zGSlYJ4EfpKG4LI6WfquulNdtmHg1yuOYlaAr33KyPJp4HeN/tivBnAHcZkoNy0hh/SbwDyCnz5PFJQQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.2.4.tgz}
name: '@next/swc-linux-x64-gnu'
version: 13.2.4
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
requiresBuild: true
dev: false
optional: true
registry.npmmirror.com/@next/swc-linux-x64-musl@13.2.4:
resolution: {integrity: sha512-gOvwIYoSxd+j14LOcvJr+ekd9fwYT1RyMAHOp7znA10+l40wkFiMONPLWiZuHxfRk+Dy7YdNdDh3ImumvL6VwA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.2.4.tgz}
name: '@next/swc-linux-x64-musl'
version: 13.2.4
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
requiresBuild: true
dev: false
optional: true
registry.npmmirror.com/@next/swc-win32-arm64-msvc@13.2.4:
resolution: {integrity: sha512-q3NJzcfClgBm4HvdcnoEncmztxrA5GXqKeiZ/hADvC56pwNALt3ngDC6t6qr1YW9V/EPDxCYeaX4zYxHciW4Dw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.2.4.tgz}
name: '@next/swc-win32-arm64-msvc'
version: 13.2.4
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: false
optional: true
registry.npmmirror.com/@next/swc-win32-ia32-msvc@13.2.4:
resolution: {integrity: sha512-/eZ5ncmHUYtD2fc6EUmAIZlAJnVT2YmxDsKs1Ourx0ttTtvtma/WKlMV5NoUsyOez0f9ExLyOpeCoz5aj+MPXw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.2.4.tgz}
name: '@next/swc-win32-ia32-msvc'
version: 13.2.4
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: false
optional: true
registry.npmmirror.com/@next/swc-win32-x64-msvc@13.2.4:
resolution: {integrity: sha512-0MffFmyv7tBLlji01qc0IaPP/LVExzvj7/R5x1Jph1bTAIj4Vu81yFQWHHQAP6r4ff9Ukj1mBK6MDNVXm7Tcvw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.2.4.tgz}
name: '@next/swc-win32-x64-msvc'
version: 13.2.4
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: false
optional: true
registry.npmmirror.com/@radix-ui/number@1.0.0:
resolution: {integrity: sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@radix-ui/number/-/number-1.0.0.tgz}
resolution: {integrity: sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA==, registry: https://registry.npmjs.org/, tarball: https://registry.npmmirror.com/@radix-ui/number/-/number-1.0.0.tgz}
name: '@radix-ui/number'
version: 1.0.0
dependencies:
@ -5738,7 +5725,7 @@ packages:
dev: false
registry.npmmirror.com/@radix-ui/primitive@1.0.0:
resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.0.0.tgz}
resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==, registry: https://registry.npmjs.org/, tarball: https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.0.0.tgz}
name: '@radix-ui/primitive'
version: 1.0.0
dependencies:
@ -5746,7 +5733,7 @@ packages:
dev: false
registry.npmmirror.com/@radix-ui/react-compose-refs@1.0.0(react@18.2.0):
resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz}
resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==, registry: https://registry.npmjs.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz}
id: registry.npmmirror.com/@radix-ui/react-compose-refs/1.0.0
name: '@radix-ui/react-compose-refs'
version: 1.0.0
@ -5758,7 +5745,7 @@ packages:
dev: false
registry.npmmirror.com/@radix-ui/react-context@1.0.0(react@18.2.0):
resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.0.0.tgz}
resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==, registry: https://registry.npmjs.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.0.0.tgz}
id: registry.npmmirror.com/@radix-ui/react-context/1.0.0
name: '@radix-ui/react-context'
version: 1.0.0
@ -5770,7 +5757,7 @@ packages:
dev: false
registry.npmmirror.com/@radix-ui/react-direction@1.0.0(react@18.2.0):
resolution: {integrity: sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.0.0.tgz}
resolution: {integrity: sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==, registry: https://registry.npmjs.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.0.0.tgz}
id: registry.npmmirror.com/@radix-ui/react-direction/1.0.0
name: '@radix-ui/react-direction'
version: 1.0.0
@ -5782,7 +5769,7 @@ packages:
dev: false
registry.npmmirror.com/@radix-ui/react-presence@1.0.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.0.0.tgz}
resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==, registry: https://registry.npmjs.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.0.0.tgz}
id: registry.npmmirror.com/@radix-ui/react-presence/1.0.0
name: '@radix-ui/react-presence'
version: 1.0.0
@ -5798,7 +5785,7 @@ packages:
dev: false
registry.npmmirror.com/@radix-ui/react-primitive@1.0.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz}
resolution: {integrity: sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==, registry: https://registry.npmjs.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz}
id: registry.npmmirror.com/@radix-ui/react-primitive/1.0.2
name: '@radix-ui/react-primitive'
version: 1.0.2
@ -5813,7 +5800,7 @@ packages:
dev: false
registry.npmmirror.com/@radix-ui/react-scroll-area@1.0.3(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-sBX9j8Q+0/jReNObEAveKIGXJtk3xUoSIx4cMKygGtO128QJyVDn01XNOFsyvihKDCTcu7SINzQ2jPAZEhIQtw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.3.tgz}
resolution: {integrity: sha512-sBX9j8Q+0/jReNObEAveKIGXJtk3xUoSIx4cMKygGtO128QJyVDn01XNOFsyvihKDCTcu7SINzQ2jPAZEhIQtw==, registry: https://registry.npmjs.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.3.tgz}
id: registry.npmmirror.com/@radix-ui/react-scroll-area/1.0.3
name: '@radix-ui/react-scroll-area'
version: 1.0.3
@ -5836,7 +5823,7 @@ packages:
dev: false
registry.npmmirror.com/@radix-ui/react-slot@1.0.1(react@18.2.0):
resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.0.1.tgz}
resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==, registry: https://registry.npmjs.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.0.1.tgz}
id: registry.npmmirror.com/@radix-ui/react-slot/1.0.1
name: '@radix-ui/react-slot'
version: 1.0.1
@ -5849,7 +5836,7 @@ packages:
dev: false
registry.npmmirror.com/@radix-ui/react-use-callback-ref@1.0.0(react@18.2.0):
resolution: {integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz}
resolution: {integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==, registry: https://registry.npmjs.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz}
id: registry.npmmirror.com/@radix-ui/react-use-callback-ref/1.0.0
name: '@radix-ui/react-use-callback-ref'
version: 1.0.0
@ -5861,7 +5848,7 @@ packages:
dev: false
registry.npmmirror.com/@radix-ui/react-use-layout-effect@1.0.0(react@18.2.0):
resolution: {integrity: sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz}
resolution: {integrity: sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==, registry: https://registry.npmjs.org/, tarball: https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz}
id: registry.npmmirror.com/@radix-ui/react-use-layout-effect/1.0.0
name: '@radix-ui/react-use-layout-effect'
version: 1.0.0
@ -5872,18 +5859,8 @@ packages:
react: 18.2.0
dev: false
registry.npmmirror.com/fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz}
name: fsevents
version: 2.3.2
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
dev: true
optional: true
registry.npmmirror.com/regenerator-runtime@0.13.11:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz}
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==, registry: https://registry.npmjs.org/, tarball: https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz}
name: regenerator-runtime
version: 0.13.11
dev: false

View File

@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
}
};

View File

@ -7,7 +7,10 @@ const ClearConversationButton = () => {
const conversationStore = useConversationStore();
const messageStore = useMessageStore();
const [showConfirmModal, setShowConfirmModal] = useState(false);
const messageList = messageStore.messageList.filter((message) => message.conversationId === conversationStore.currentConversationId);
const messageList = messageStore.messageList.filter(
(message) =>
message.conversationId === conversationStore.currentConversationId
);
return (
<>
@ -19,7 +22,11 @@ const ClearConversationButton = () => {
<Icon.GiBroom className="w-6 h-auto" />
</button>
{showConfirmModal && <ClearConversationConfirmModal close={() => setShowConfirmModal(false)} />}
{showConfirmModal && (
<ClearConversationConfirmModal
close={() => setShowConfirmModal(false)}
/>
)}
</>
);
};

View File

@ -13,7 +13,9 @@ const ClearConversationConfirmModal = (props: Props) => {
const messageStore = useMessageStore();
const handleClearMessages = () => {
messageStore.clearMessage((item) => item.conversationId !== conversationStore.currentConversationId);
messageStore.clearMessage(
(item) => item.conversationId !== conversationStore.currentConversationId
);
close();
};
@ -21,7 +23,9 @@ const ClearConversationConfirmModal = (props: Props) => {
<Modal title="Clear messages" className="!w-96" onClose={close}>
<div>
<div className="w-full flex flex-col justify-start items-start mt-2">
<p className="text-gray-500">Are you sure to clear the messages in current conversation?</p>
<p className="text-gray-500">
Are you sure to clear the messages in current conversation?
</p>
</div>
<div className="w-full flex flex-row justify-end items-center mt-4 space-x-2">
<button className="btn btn-outline" onClick={close}>

View File

@ -4,15 +4,23 @@ import ClearDataConfirmModal from "./ClearDataConfirmModal";
const ClearDataButton = () => {
const { t } = useTranslation();
const [showClearDataConfirmModal, setShowClearDataConfirmModal] = useState(false);
const [showClearDataConfirmModal, setShowClearDataConfirmModal] =
useState(false);
return (
<>
<button className="btn btn-error" onClick={() => setShowClearDataConfirmModal(true)}>
<button
className="btn btn-error"
onClick={() => setShowClearDataConfirmModal(true)}
>
{t("common.clear")}
</button>
{showClearDataConfirmModal && <ClearDataConfirmModal close={() => setShowClearDataConfirmModal(false)} />}
{showClearDataConfirmModal && (
<ClearDataConfirmModal
close={() => setShowClearDataConfirmModal(false)}
/>
)}
</>
);
};

View File

@ -23,7 +23,10 @@ const ClearDataConfirmModal = (props: Props) => {
<Modal title="Clear all data" className="!w-96" onClose={close}>
<div>
<div className="w-full flex flex-col justify-start items-start mt-2">
<p className="text-gray-500">SQL Chat saves all your data in your local browser. Are you sure to clear all of them?</p>
<p className="text-gray-500">
SQL Chat saves all your data in your local browser. Are you sure to
clear all of them?
</p>
</div>
<div className="w-full flex flex-row justify-end items-center mt-4 space-x-2">
<button className="btn btn-outline" onClick={close}>

View File

@ -21,7 +21,10 @@ export const CodeBlock = (props: Props) => {
// Only show execute button in the following situations:
// * SQL code;
// * Connection setup;
const showExecuteButton = currentConnectionCtx?.connection && currentConnectionCtx?.database && language.toUpperCase() === "SQL";
const showExecuteButton =
currentConnectionCtx?.connection &&
currentConnectionCtx?.database &&
language.toUpperCase() === "SQL";
const copyToClipboard = () => {
copy(value);
@ -67,7 +70,11 @@ export const CodeBlock = (props: Props) => {
)}
</div>
</div>
<SyntaxHighlighter language={language.toLowerCase()} style={oneDark} customStyle={{ margin: 0 }}>
<SyntaxHighlighter
language={language.toLowerCase()}
style={oneDark}
customStyle={{ margin: 0 }}
>
{value}
</SyntaxHighlighter>
</div>

View File

@ -21,9 +21,12 @@ const ConnectionSidebar = () => {
const [state, setState] = useState<State>({
showSettingModal: false,
});
const [isRequestingDatabase, setIsRequestingDatabase] = useState<boolean>(false);
const [isRequestingDatabase, setIsRequestingDatabase] =
useState<boolean>(false);
const currentConnectionCtx = connectionStore.currentConnectionCtx;
const databaseList = connectionStore.databaseList.filter((database) => database.connectionId === currentConnectionCtx?.connection.id);
const databaseList = connectionStore.databaseList.filter(
(database) => database.connectionId === currentConnectionCtx?.connection.id
);
useEffect(() => {
const handleWindowResize = () => {
@ -47,9 +50,11 @@ const ConnectionSidebar = () => {
useEffect(() => {
if (currentConnectionCtx?.connection) {
setIsRequestingDatabase(true);
connectionStore.getOrFetchDatabaseList(currentConnectionCtx.connection).finally(() => {
setIsRequestingDatabase(false);
});
connectionStore
.getOrFetchDatabaseList(currentConnectionCtx.connection)
.finally(() => {
setIsRequestingDatabase(false);
});
} else {
setIsRequestingDatabase(false);
}
@ -67,8 +72,12 @@ const ConnectionSidebar = () => {
return;
}
const databaseList = await connectionStore.getOrFetchDatabaseList(currentConnectionCtx.connection);
const database = databaseList.find((database) => database.name === databaseName);
const databaseList = await connectionStore.getOrFetchDatabaseList(
currentConnectionCtx.connection
);
const database = databaseList.find(
(database) => database.name === databaseName
);
connectionStore.setCurrentConnectionCtx({
connection: currentConnectionCtx.connection,
database: database,
@ -107,7 +116,8 @@ const ConnectionSidebar = () => {
<div className="w-full grow">
{isRequestingDatabase && (
<div className="w-full h-12 flex flex-row justify-start items-center px-4 sticky top-0 border z-1 mb-4 mt-2 rounded-lg text-sm text-gray-600 dark:text-gray-400">
<Icon.BiLoaderAlt className="w-4 h-auto animate-spin mr-1" /> {t("common.loading")}
<Icon.BiLoaderAlt className="w-4 h-auto animate-spin mr-1" />{" "}
{t("common.loading")}
</div>
)}
{databaseList.length > 0 && (
@ -121,7 +131,9 @@ const ConnectionSidebar = () => {
value: database.name,
};
})}
onValueChange={(databaseName) => handleDatabaseNameSelect(databaseName)}
onValueChange={(databaseName) =>
handleDatabaseNameSelect(databaseName)
}
placeholder={t("connection.select-database") || ""}
/>
</div>
@ -168,7 +180,9 @@ const ConnectionSidebar = () => {
</div>
</Drawer>
{state.showSettingModal && <SettingModal close={() => toggleSettingModal(false)} />}
{state.showSettingModal && (
<SettingModal close={() => toggleSettingModal(false)} />
)}
</>
);
};

View File

@ -14,7 +14,9 @@ const Header = (props: Props) => {
const conversationStore = useConversationStore();
const isDarkMode = useDarkMode();
const currentConversationId = conversationStore.currentConversationId;
const title = conversationStore.getConversationById(currentConversationId)?.title || "SQL Chat";
const title =
conversationStore.getConversationById(currentConversationId)?.title ||
"SQL Chat";
useEffect(() => {
document.title = `${title}`;
@ -36,14 +38,24 @@ const Header = (props: Props) => {
<span className="w-auto text-left block lg:hidden">{title}</span>
<GitHubStarBadge className="hidden lg:flex ml-2" />
</div>
<span className="w-auto text-center h-8 p-1 hidden lg:block">{title}</span>
<span className="w-auto text-center h-8 p-1 hidden lg:block">
{title}
</span>
<div className="mr-2 sm:mr-3 relative flex flex-row justify-end items-center">
<a
href="https://www.bytebase.com?source=sqlchat"
className="flex flex-row justify-center items-center h-10 px-3 py-1 rounded-md whitespace-nowrap hover:bg-gray-100 dark:hover:bg-zinc-700"
target="_blank"
>
<img className="h-5 sm:h-6 w-auto" src={isDarkMode ? "/craft-by-bytebase-dark-mode.webp" : "/craft-by-bytebase.webp"} alt="" />
<img
className="h-5 sm:h-6 w-auto"
src={
isDarkMode
? "/craft-by-bytebase-dark-mode.webp"
: "/craft-by-bytebase.webp"
}
alt=""
/>
</a>
</div>
</div>

View File

@ -2,7 +2,12 @@ import { useEffect, useRef, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import TextareaAutosize from "react-textarea-autosize";
import { useConversationStore, useConnectionStore, useMessageStore, useUserStore } from "@/store";
import {
useConversationStore,
useConnectionStore,
useMessageStore,
useUserStore,
} from "@/store";
import { CreatorRole } from "@/types";
import { generateUUID } from "@/utils";
import Icon from "../Icon";
@ -34,13 +39,18 @@ const MessageTextarea = (props: Props) => {
};
const handleSend = async () => {
let conversation = conversationStore.getConversationById(conversationStore.currentConversationId);
let conversation = conversationStore.getConversationById(
conversationStore.currentConversationId
);
if (!conversation) {
const currentConnectionCtx = connectionStore.currentConnectionCtx;
if (!currentConnectionCtx) {
conversation = conversationStore.createConversation();
} else {
conversation = conversationStore.createConversation(currentConnectionCtx.connection.id, currentConnectionCtx.database?.name);
conversation = conversationStore.createConversation(
currentConnectionCtx.connection.id,
currentConnectionCtx.database?.name
);
}
}
if (!value) {

View File

@ -4,7 +4,13 @@ import { useTranslation } from "react-i18next";
import { toast } from "react-hot-toast";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { useConversationStore, useConnectionStore, useMessageStore, useUserStore, useSettingStore } from "@/store";
import {
useConversationStore,
useConnectionStore,
useMessageStore,
useUserStore,
useSettingStore,
} from "@/store";
import { Message } from "@/types";
import Dropdown, { DropdownItem } from "../kit/Dropdown";
import Icon from "../Icon";
@ -25,7 +31,10 @@ const MessageView = (props: Props) => {
const connectionStore = useConnectionStore();
const messageStore = useMessageStore();
const isCurrentUser = message.creatorId === userStore.currentUser.id;
const connection = connectionStore.getConnectionById(conversationStore.getConversationById(message.conversationId)?.connectionId || "");
const connection = connectionStore.getConnectionById(
conversationStore.getConversationById(message.conversationId)
?.connectionId || ""
);
const copyMessage = () => {
navigator.clipboard.writeText(message.content);
@ -83,9 +92,16 @@ const MessageView = (props: Props) => {
<>
<div className="flex justify-center items-center mr-2 shrink-0">
{connection ? (
<EngineIcon className="w-10 h-auto p-1 border dark:border-zinc-700 rounded-full" engine={connection.engineType} />
<EngineIcon
className="w-10 h-auto p-1 border dark:border-zinc-700 rounded-full"
engine={connection.engineType}
/>
) : (
<img className="w-10 h-auto p-1" src="/chat-logo-bot.webp" alt="" />
<img
className="w-10 h-auto p-1"
src="/chat-logo-bot.webp"
alt=""
/>
)}
</div>
{message.status === "LOADING" && message.content === "" ? (
@ -97,20 +113,29 @@ const MessageView = (props: Props) => {
<div className="w-auto max-w-[calc(100%-2rem)] flex flex-col justify-start items-start">
<ReactMarkdown
className={`w-auto max-w-full bg-gray-100 dark:bg-zinc-700 px-4 py-2 rounded-lg prose prose-neutral dark:prose-invert ${
message.status === "FAILED" && "border border-red-400 bg-red-100 text-red-500"
message.status === "FAILED" &&
"border border-red-400 bg-red-100 text-red-500"
}`}
remarkPlugins={[remarkGfm]}
components={{
pre({ node, className, children, ...props }) {
const child = children[0] as ReactElement;
const match = /language-(\w+)/.exec(child.props.className || "");
const match = /language-(\w+)/.exec(
child.props.className || ""
);
const language = match ? match[1] : "SQL";
return (
<pre className={`${className || ""} w-full p-0 my-1`} {...props}>
<pre
className={`${className || ""} w-full p-0 my-1`}
{...props}
>
<CodeBlock
key={Math.random()}
language={language || "SQL"}
value={String(child.props.children).replace(/\n$/, "")}
value={String(child.props.children).replace(
/\n$/,
""
)}
{...props}
/>
</pre>
@ -124,7 +149,9 @@ const MessageView = (props: Props) => {
{message.content}
</ReactMarkdown>
<span className="self-end text-sm text-gray-400 pt-1 pr-1">
{dayjs(message.createdAt).locale(settingStore.setting.locale).format("lll")}
{dayjs(message.createdAt)
.locale(settingStore.setting.locale)
.format("lll")}
</span>
</div>
<div className="invisible group-hover:visible">

View File

@ -10,7 +10,10 @@ const ThreeDotsLoader = () => {
const theme = settingStore.setting.theme;
let appearance = theme;
if (theme === "system") {
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
if (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
appearance = "dark";
} else {
appearance = "light";
@ -24,7 +27,14 @@ const ThreeDotsLoader = () => {
}
}, [settingStore.setting.theme]);
return <ThreeDots wrapperClass="dark:opacity-60" width="24" height="24" color={color} />;
return (
<ThreeDots
wrapperClass="dark:opacity-60"
width="24"
height="24"
color={color}
/>
);
};
export default ThreeDotsLoader;

View File

@ -35,9 +35,13 @@ const ConversationView = () => {
const [isStickyAtBottom, setIsStickyAtBottom] = useState<boolean>(true);
const [showHeaderShadow, setShowHeaderShadow] = useState<boolean>(false);
const conversationViewRef = useRef<HTMLDivElement>(null);
const currentConversation = conversationStore.getConversationById(conversationStore.currentConversationId);
const currentConversation = conversationStore.getConversationById(
conversationStore.currentConversationId
);
const messageList = currentConversation
? messageStore.messageList.filter((message) => message.conversationId === currentConversation.id)
? messageStore.messageList.filter(
(message) => message.conversationId === currentConversation.id
)
: [];
const lastMessage = last(messageList);
@ -63,13 +67,21 @@ const ConversationView = () => {
}
setShowHeaderShadow((conversationViewRef.current?.scrollTop || 0) > 0);
setIsStickyAtBottom(
conversationViewRef.current.scrollTop + conversationViewRef.current.clientHeight >= conversationViewRef.current.scrollHeight
conversationViewRef.current.scrollTop +
conversationViewRef.current.clientHeight >=
conversationViewRef.current.scrollHeight
);
};
conversationViewRef.current?.addEventListener("scroll", handleConversationViewScroll);
conversationViewRef.current?.addEventListener(
"scroll",
handleConversationViewScroll
);
return () => {
conversationViewRef.current?.removeEventListener("scroll", handleConversationViewScroll);
conversationViewRef.current?.removeEventListener(
"scroll",
handleConversationViewScroll
);
};
}, []);
@ -77,7 +89,8 @@ const ConversationView = () => {
if (!conversationViewRef.current) {
return;
}
conversationViewRef.current.scrollTop = conversationViewRef.current.scrollHeight;
conversationViewRef.current.scrollTop =
conversationViewRef.current.scrollHeight;
}, [currentConversation, lastMessage?.id]);
useEffect(() => {
@ -86,14 +99,17 @@ const ConversationView = () => {
}
if (lastMessage?.status === "LOADING" && isStickyAtBottom) {
conversationViewRef.current.scrollTop = conversationViewRef.current.scrollHeight;
conversationViewRef.current.scrollTop =
conversationViewRef.current.scrollHeight;
}
}, [lastMessage?.status, lastMessage?.content, isStickyAtBottom]);
useEffect(() => {
if (
currentConversation?.connectionId === connectionStore.currentConnectionCtx?.connection.id &&
currentConversation?.databaseName === connectionStore.currentConnectionCtx?.database?.name
currentConversation?.connectionId ===
connectionStore.currentConnectionCtx?.connection.id &&
currentConversation?.databaseName ===
connectionStore.currentConnectionCtx?.database?.name
) {
return;
}
@ -101,14 +117,18 @@ const ConversationView = () => {
// Auto select the first conversation when the current connection changes.
const conversationList = conversationStore.conversationList.filter(
(conversation) =>
conversation.connectionId === connectionStore.currentConnectionCtx?.connection.id &&
conversation.databaseName === connectionStore.currentConnectionCtx?.database?.name
conversation.connectionId ===
connectionStore.currentConnectionCtx?.connection.id &&
conversation.databaseName ===
connectionStore.currentConnectionCtx?.database?.name
);
conversationStore.setCurrentConversationId(head(conversationList)?.id);
}, [currentConversation, connectionStore.currentConnectionCtx]);
const sendMessageToCurrentConversation = async () => {
const currentConversation = conversationStore.getConversationById(conversationStore.getState().currentConversationId);
const currentConversation = conversationStore.getConversationById(
conversationStore.getState().currentConversationId
);
if (!currentConversation) {
return;
}
@ -116,8 +136,14 @@ const ConversationView = () => {
return;
}
const messageList = messageStore.getState().messageList.filter((message) => message.conversationId === currentConversation.id);
const promptGenerator = getPromptGeneratorOfAssistant(getAssistantById(currentConversation.assistantId)!);
const messageList = messageStore
.getState()
.messageList.filter(
(message) => message.conversationId === currentConversation.id
);
const promptGenerator = getPromptGeneratorOfAssistant(
getAssistantById(currentConversation.assistantId)!
);
let prompt = promptGenerator();
let tokens = 0;
@ -135,7 +161,9 @@ const ConversationView = () => {
if (connectionStore.currentConnectionCtx?.database) {
let schema = "";
try {
const tables = await connectionStore.getOrFetchDatabaseSchema(connectionStore.currentConnectionCtx?.database);
const tables = await connectionStore.getOrFetchDatabaseSchema(
connectionStore.currentConnectionCtx?.database
);
for (const table of tables) {
if (tokens < MAX_TOKENS / 2) {
tokens += countTextTokens(schema + table.structure);
@ -187,7 +215,8 @@ const ConversationView = () => {
if (!rawRes.ok) {
console.error(rawRes);
let errorMessage = "Failed to request message, please check your network.";
let errorMessage =
"Failed to request message, please check your network.";
try {
const res = await rawRes.json();
errorMessage = res.error.message;
@ -253,14 +282,22 @@ const ConversationView = () => {
</div>
<div className="p-2 w-full h-auto grow max-w-4xl py-1 px-4 sm:px-8 mx-auto">
{messageList.length === 0 ? (
<EmptyView className="mt-16" sendMessage={sendMessageToCurrentConversation} />
<EmptyView
className="mt-16"
sendMessage={sendMessageToCurrentConversation}
/>
) : (
messageList.map((message) => <MessageView key={message.id} message={message} />)
messageList.map((message) => (
<MessageView key={message.id} message={message} />
))
)}
</div>
<div className="sticky bottom-0 flex flex-row justify-center items-center w-full max-w-4xl py-2 pb-4 px-4 sm:px-8 mx-auto bg-white dark:bg-zinc-800 bg-opacity-80 backdrop-blur">
<ClearConversationButton />
<MessageTextarea disabled={lastMessage?.status === "LOADING"} sendMessage={sendMessageToCurrentConversation} />
<MessageTextarea
disabled={lastMessage?.status === "LOADING"}
sendMessage={sendMessageToCurrentConversation}
/>
</div>
</div>
);

View File

@ -51,7 +51,8 @@ const CreateConnectionModal = (props: Props) => {
const { t } = useTranslation();
const connectionStore = useConnectionStore();
const [connection, setConnection] = useState<Connection>(defaultConnection);
const [showDeleteConnectionModal, setShowDeleteConnectionModal] = useState(false);
const [showDeleteConnectionModal, setShowDeleteConnectionModal] =
useState(false);
const [sslType, setSSLType] = useState<SSLType>("none");
const [selectedSSLField, setSelectedSSLField] = useState<SSLFieldType>("ca");
const [isRequesting, setIsRequesting] = useState(false);
@ -105,7 +106,11 @@ const CreateConnectionModal = (props: Props) => {
}
const file = files[0];
if (file.type.startsWith("audio/") || file.type.startsWith("video/") || file.type.startsWith("image/")) {
if (
file.type.startsWith("audio/") ||
file.type.startsWith("video/") ||
file.type.startsWith("image/")
) {
toast.error(`Invalid file type:${file.type}`);
return;
}
@ -177,7 +182,10 @@ const CreateConnectionModal = (props: Props) => {
}
// Set the created connection as the current connection.
const databaseList = await connectionStore.getOrFetchDatabaseList(connection, true);
const databaseList = await connectionStore.getOrFetchDatabaseList(
connection,
true
);
connectionStore.setCurrentConnectionCtx({
connection: connection,
database: head(databaseList),
@ -203,11 +211,19 @@ const CreateConnectionModal = (props: Props) => {
return (
<>
<Modal title={isEditing ? t("connection.edit") : t("connection.new")} onClose={close}>
<Modal
title={isEditing ? t("connection.edit") : t("connection.new")}
onClose={close}
>
<div className="w-full flex flex-col justify-start items-start space-y-3 mt-2">
<DataStorageBanner className="rounded-lg bg-white border dark:border-zinc-700 py-2 !justify-start" alwaysShow={true} />
<DataStorageBanner
className="rounded-lg bg-white border dark:border-zinc-700 py-2 !justify-start"
alwaysShow={true}
/>
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">{t("connection.database-type")}</label>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t("connection.database-type")}
</label>
<Select
className="w-full"
value={connection.engineType}
@ -216,24 +232,46 @@ const CreateConnectionModal = (props: Props) => {
{ value: Engine.PostgreSQL, label: "PostgreSQL" },
{ value: Engine.MSSQL, label: "MSSQL" },
]}
onValueChange={(value) => setPartialConnection({ engineType: value as Engine })}
onValueChange={(value) =>
setPartialConnection({ engineType: value as Engine })
}
/>
</div>
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">{t("connection.title")}</label>
<TextField placeholder="Title" value={connection.title} onChange={(value) => setPartialConnection({ title: value })} />
<label className="block text-sm font-medium text-gray-700 mb-1">
{t("connection.title")}
</label>
<TextField
placeholder="Title"
value={connection.title}
onChange={(value) => setPartialConnection({ title: value })}
/>
</div>
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">{t("connection.host")}</label>
<TextField placeholder="Connection host" value={connection.host} onChange={(value) => setPartialConnection({ host: value })} />
<label className="block text-sm font-medium text-gray-700 mb-1">
{t("connection.host")}
</label>
<TextField
placeholder="Connection host"
value={connection.host}
onChange={(value) => setPartialConnection({ host: value })}
/>
</div>
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">{t("connection.port")}</label>
<TextField placeholder="Connection port" value={connection.port} onChange={(value) => setPartialConnection({ port: value })} />
<label className="block text-sm font-medium text-gray-700 mb-1">
{t("connection.port")}
</label>
<TextField
placeholder="Connection port"
value={connection.port}
onChange={(value) => setPartialConnection({ port: value })}
/>
</div>
{showDatabaseField && (
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">{t("connection.database-name")}</label>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t("connection.database-name")}
</label>
<TextField
placeholder="Connection database"
value={connection.database || ""}
@ -242,7 +280,9 @@ const CreateConnectionModal = (props: Props) => {
</div>
)}
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">{t("connection.username")}</label>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t("connection.username")}
</label>
<TextField
placeholder="Connection username"
value={connection.username || ""}
@ -250,7 +290,9 @@ const CreateConnectionModal = (props: Props) => {
/>
</div>
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">{t("connection.password")}</label>
<label className="block text-sm font-medium text-gray-700 mb-1">
{t("connection.password")}
</label>
<TextField
placeholder="Connection password"
type="password"
@ -259,10 +301,15 @@ const CreateConnectionModal = (props: Props) => {
/>
</div>
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">SSL</label>
<label className="block text-sm font-medium text-gray-700 mb-1">
SSL
</label>
<div className="w-full flex flex-row justify-start items-start flex-wrap">
{SSLTypeOptions.map((option) => (
<label key={option.value} className="w-auto flex flex-row justify-start items-center cursor-pointer mr-3 mb-3">
<label
key={option.value}
className="w-auto flex flex-row justify-start items-center cursor-pointer mr-3 mb-3"
>
<input
type="radio"
className="radio w-4 h-4 mr-1"
@ -279,7 +326,8 @@ const CreateConnectionModal = (props: Props) => {
<div className="text-sm space-x-3 mb-2">
<span
className={`leading-6 pb-1 border-b-2 border-transparent cursor-pointer opacity-60 hover:opacity-80 ${
selectedSSLField === "ca" && "!border-indigo-600 !opacity-100"
selectedSSLField === "ca" &&
"!border-indigo-600 !opacity-100"
} `}
onClick={() => setSelectedSSLField("ca")}
>
@ -289,7 +337,8 @@ const CreateConnectionModal = (props: Props) => {
<>
<span
className={`leading-6 pb-1 border-b-2 border-transparent cursor-pointer opacity-60 hover:opacity-80 ${
selectedSSLField === "key" && "!border-indigo-600 !opacity-100"
selectedSSLField === "key" &&
"!border-indigo-600 !opacity-100"
}`}
onClick={() => setSelectedSSLField("key")}
>
@ -297,7 +346,8 @@ const CreateConnectionModal = (props: Props) => {
</span>
<span
className={`leading-6 pb-1 border-b-2 border-transparent cursor-pointer opacity-60 hover:opacity-80 ${
selectedSSLField === "cert" && "!border-indigo-600 !opacity-100"
selectedSSLField === "cert" &&
"!border-indigo-600 !opacity-100"
}`}
onClick={() => setSelectedSSLField("cert")}
>
@ -311,18 +361,26 @@ const CreateConnectionModal = (props: Props) => {
className="w-full border resize-none rounded-lg text-sm p-3"
minRows={3}
maxRows={3}
value={(connection.ssl && connection.ssl[selectedSSLField]) ?? ""}
value={
(connection.ssl && connection.ssl[selectedSSLField]) ?? ""
}
onChange={handleSSLValueChange}
/>
<div
className={`${
connection.ssl && connection.ssl[selectedSSLField] && "hidden"
connection.ssl &&
connection.ssl[selectedSSLField] &&
"hidden"
} absolute top-3 left-4 text-gray-400 text-sm leading-6 pointer-events-none`}
>
<span className="">Input or </span>
<label className="pointer-events-auto border border-dashed px-2 py-1 rounded-lg cursor-pointer hover:border-gray-600 hover:text-gray-600">
upload file
<input className="hidden" type="file" onChange={handleSSLFileInputChange} />
<input
className="hidden"
type="file"
onChange={handleSSLFileInputChange}
/>
</label>
</div>
</div>
@ -330,14 +388,18 @@ const CreateConnectionModal = (props: Props) => {
)}
{connection.engineType === Engine.MSSQL && (
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">Encrypt</label>
<label className="block text-sm font-medium text-gray-700 mb-1">
Encrypt
</label>
<div className="w-full flex flex-row justify-start items-start flex-wrap">
<label className="flex items-center">
<input
type="checkbox"
className="form-checkbox h-4 w-4 text-indigo-600 transition duration-150 ease-in-out"
checked={connection.encrypt}
onChange={(e) => setPartialConnection({ encrypt: e.target.checked })}
onChange={(e) =>
setPartialConnection({ encrypt: e.target.checked })
}
/>
<span className="ml-2 text-sm">Encrypt connection</span>
</label>
@ -349,7 +411,10 @@ const CreateConnectionModal = (props: Props) => {
<div className="modal-action w-full flex flex-row justify-between items-center space-x-2">
<div>
{isEditing && (
<button className="btn btn-outline" onClick={() => setShowDeleteConnectionModal(true)}>
<button
className="btn btn-outline"
onClick={() => setShowDeleteConnectionModal(true)}
>
Delete
</button>
)}
@ -358,8 +423,14 @@ const CreateConnectionModal = (props: Props) => {
<button className="btn btn-outline" onClick={close}>
{t("common.close")}
</button>
<button className="btn" disabled={isRequesting || !allowSave} onClick={handleCreateConnection}>
{isRequesting && <Icon.BiLoaderAlt className="w-4 h-auto animate-spin mr-1" />}
<button
className="btn"
disabled={isRequesting || !allowSave}
onClick={handleCreateConnection}
>
{isRequesting && (
<Icon.BiLoaderAlt className="w-4 h-auto animate-spin mr-1" />
)}
{t("common.save")}
</button>
</div>

View File

@ -10,7 +10,10 @@ interface Props {
const DataStorageBanner = (props: Props) => {
const { className, alwaysShow } = props;
const { t } = useTranslation();
const [hideBanner, setHideBanner] = useLocalStorage("hide-local-storage-banner", false);
const [hideBanner, setHideBanner] = useLocalStorage(
"hide-local-storage-banner",
false
);
const show = alwaysShow || !hideBanner;
return (
@ -24,7 +27,10 @@ const DataStorageBanner = (props: Props) => {
{t("banner.data-storage")}
</span>
{!alwaysShow && (
<button className="absolute right-2 sm:right-4 opacity-60 hover:opacity-100" onClick={() => setHideBanner(true)}>
<button
className="absolute right-2 sm:right-4 opacity-60 hover:opacity-100"
onClick={() => setHideBanner(true)}
>
<Icon.BiX className="w-6 h-auto" />
</button>
)}

View File

@ -1,11 +1,19 @@
import { useConversationStore, useConnectionStore, useMessageStore, useUserStore } from "@/store";
import {
useConversationStore,
useConnectionStore,
useMessageStore,
useUserStore,
} from "@/store";
import { CreatorRole } from "@/types";
import { generateUUID } from "@/utils";
import useDarkMode from "@/hooks/useDarkmode";
import Icon from "./Icon";
// examples are used to show some examples to the user.
const examples = ["Give me an example schema about employee", "How to create a view in MySQL?"];
const examples = [
"Give me an example schema about employee",
"How to create a view in MySQL?",
];
interface Props {
className?: string;
@ -21,13 +29,18 @@ const EmptyView = (props: Props) => {
const isDarkMode = useDarkMode();
const handleExampleClick = async (content: string) => {
let conversation = conversationStore.getConversationById(conversationStore.currentConversationId);
let conversation = conversationStore.getConversationById(
conversationStore.currentConversationId
);
if (!conversation) {
const currentConnectionCtx = connectionStore.currentConnectionCtx;
if (!currentConnectionCtx) {
conversation = conversationStore.createConversation();
} else {
conversation = conversationStore.createConversation(currentConnectionCtx.connection.id, currentConnectionCtx.database?.name);
conversation = conversationStore.createConversation(
currentConnectionCtx.connection.id,
currentConnectionCtx.database?.name
);
}
}
@ -44,9 +57,20 @@ const EmptyView = (props: Props) => {
};
return (
<div className={`${className || ""} w-full h-full flex flex-col justify-start items-center`}>
<div
className={`${
className || ""
} w-full h-full flex flex-col justify-start items-center`}
>
<div className="w-96 max-w-full font-medium leading-loose mb-8">
<img src={isDarkMode ? "/chat-logo-and-text-dark-mode.webp" : "/chat-logo-and-text.webp"} alt="sql-chat-logo" />
<img
src={
isDarkMode
? "/chat-logo-and-text-dark-mode.webp"
: "/chat-logo-and-text.webp"
}
alt="sql-chat-logo"
/>
</div>
<div className="w-full grid grid-cols-2 sm:grid-cols-3 gap-4">
<div className="w-full flex flex-col justify-start items-center">

View File

@ -13,7 +13,7 @@ const EngineIcon = (props: Props) => {
return <Icon.DiMysql className={className} />;
} else if (engine === Engine.PostgreSQL) {
return <Icon.DiPostgresql className={className} />;
}else if (engine === Engine.MSSQL) {
} else if (engine === Engine.MSSQL) {
return <Icon.DiMsqlServer className={className} />;
} else {
return <Icon.DiDatabase className={className} />;

View File

@ -22,7 +22,9 @@ const DataTableView = (props: Props) => {
return rawResults.length === 0 ? (
<div className="w-full flex flex-col justify-center items-center py-6 pt-10">
<Icon.BsBox2 className="w-7 h-auto opacity-70" />
<span className="text-sm font-mono text-gray-500 mt-2">{t("execution.message.no-data")}</span>
<span className="text-sm font-mono text-gray-500 mt-2">
{t("execution.message.no-data")}
</span>
</div>
) : (
<DataTable

View File

@ -10,7 +10,11 @@ const ExecutionWarningBanner = (props: Props) => {
const { t } = useTranslation();
return (
<div className={`${className || ""} relative w-full flex flex-row justify-start items-center px-4 py-2 bg-yellow-100 dark:bg-zinc-700`}>
<div
className={`${
className || ""
} relative w-full flex flex-row justify-start items-center px-4 py-2 bg-yellow-100 dark:bg-zinc-700`}
>
<span className="text-sm leading-6 pr-4">
<Icon.IoInformationCircleOutline className="inline-block h-5 w-auto -mt-0.5 mr-0.5 opacity-80" />
{t("banner.non-select-sql-warning")}

View File

@ -6,7 +6,13 @@ interface Props {
const NotificationView = (props: Props) => {
const { message, style } = props;
const additionalStyle = style === "error" ? "text-red-500" : "text-gray-500";
return <p className={`${additionalStyle} w-full pl-4 mt-4 font-mono text-sm whitespace-pre-wrap`}>{message}</p>;
return (
<p
className={`${additionalStyle} w-full pl-4 mt-4 font-mono text-sm whitespace-pre-wrap`}
>
{message}
</p>
);
};
export default NotificationView;

View File

@ -15,12 +15,15 @@ const GitHubStarBadge = (props: Props) => {
const getRepoStarCount = async () => {
let starCount = 0;
try {
const { data } = await axios.get(`https://api.github.com/repos/sqlchat/sqlchat`, {
headers: {
Accept: "application/vnd.github.v3.star+json",
Authorization: "",
},
});
const { data } = await axios.get(
`https://api.github.com/repos/sqlchat/sqlchat`,
{
headers: {
Accept: "application/vnd.github.v3.star+json",
Authorization: "",
},
}
);
starCount = data.stargazers_count as number;
} catch (error) {
// do nth
@ -47,7 +50,11 @@ const GitHubStarBadge = (props: Props) => {
<span className="mt-px">Star</span>
</span>
<div className="h-full block px-2 mt-px font-medium">
{isRequesting ? <Icon.BiLoaderAlt className="w-3 h-auto animate-spin opacity-70" /> : stars}
{isRequesting ? (
<Icon.BiLoaderAlt className="w-3 h-auto animate-spin opacity-70" />
) : (
stars
)}
</div>
</a>
);

View File

@ -46,7 +46,14 @@ const LocaleSelector = () => {
settingStore.setLocale(locale);
};
return <Select className="w-28" value={locale} itemList={localeItemList} onValueChange={handleLocaleChange} />;
return (
<Select
className="w-28"
value={locale}
itemList={localeItemList}
onValueChange={handleLocaleChange}
/>
);
};
export default LocaleSelector;

View File

@ -8,7 +8,9 @@ import TextField from "./kit/TextField";
const OpenAIApiConfigView = () => {
const { t } = useTranslation();
const settingStore = useSettingStore();
const [openAIApiConfig, setOpenAIApiConfig] = useState(settingStore.setting.openAIApiConfig);
const [openAIApiConfig, setOpenAIApiConfig] = useState(
settingStore.setting.openAIApiConfig
);
useDebounce(
() => {
@ -27,7 +29,9 @@ const OpenAIApiConfigView = () => {
return (
<>
<h3 className="pl-4 text-sm text-gray-500">{t("setting.openai-api-configuration.self")}</h3>
<h3 className="pl-4 text-sm text-gray-500">
{t("setting.openai-api-configuration.self")}
</h3>
<div className="w-full border border-gray-200 dark:border-zinc-700 p-4 rounded-lg">
<div className="flex flex-col">
<label className="mb-1">Key</label>

View File

@ -9,7 +9,10 @@ interface Props {
const ProductHuntBanner = (props: Props) => {
const { className } = props;
const { t } = useTranslation();
const [hideBanner, setHideBanner] = useLocalStorage("hide-product-hunt-banner", false);
const [hideBanner, setHideBanner] = useLocalStorage(
"hide-product-hunt-banner",
false
);
const show = !hideBanner;
return (
@ -25,7 +28,10 @@ const ProductHuntBanner = (props: Props) => {
>
{t("banner.product-hunt")}
</a>
<button className="absolute right-2 sm:right-4 opacity-60 hover:opacity-100" onClick={() => setHideBanner(true)}>
<button
className="absolute right-2 sm:right-4 opacity-60 hover:opacity-100"
onClick={() => setHideBanner(true)}
>
<Icon.BiX className="w-6 h-auto" />
</button>
</div>

View File

@ -16,12 +16,17 @@ import ExecutionWarningBanner from "./ExecutionView/ExecutionWarningBanner";
const QueryDrawer = () => {
const { t } = useTranslation();
const queryStore = useQueryStore();
const [executionResult, setExecutionResult] = useState<ExecutionResult | undefined>(undefined);
const [executionResult, setExecutionResult] = useState<
ExecutionResult | undefined
>(undefined);
const [statement, setStatement] = useState<string>("");
const [isLoading, setIsLoading] = useState(false);
const context = queryStore.context;
const executionMessage = executionResult ? getMessageFromExecutionResult(executionResult) : "";
const showExecutionWarningBanner = statement.trim() && !checkStatementIsSelect(statement);
const executionMessage = executionResult
? getMessageFromExecutionResult(executionResult)
: "";
const showExecutionWarningBanner =
statement.trim() && !checkStatementIsSelect(statement);
useEffect(() => {
if (!queryStore.showDrawer) {
@ -84,25 +89,40 @@ const QueryDrawer = () => {
const close = () => queryStore.toggleDrawer(false);
return (
<Drawer open={queryStore.showDrawer} anchor="right" className="w-full" onClose={close}>
<Drawer
open={queryStore.showDrawer}
anchor="right"
className="w-full"
onClose={close}
>
<div className="dark:text-gray-300 w-screen sm:w-[calc(60vw)] lg:w-[calc(50vw)] 2xl:w-[calc(40vw)] max-w-full flex flex-col justify-start items-start p-4">
<button className="w-8 h-8 p-1 bg-zinc-600 text-gray-100 rounded-full hover:opacity-80" onClick={close}>
<button
className="w-8 h-8 p-1 bg-zinc-600 text-gray-100 rounded-full hover:opacity-80"
onClick={close}
>
<Icon.IoMdClose className="w-full h-auto" />
</button>
<h3 className="font-bold text-2xl mt-4">{t("execution.title")}</h3>
{!context ? (
<div className="w-full flex flex-col justify-center items-center py-6 pt-10">
<Icon.BiSad className="w-7 h-auto opacity-70" />
<span className="text-sm font-mono text-gray-500 mt-2">{t("execution.message.no-connection")}</span>
<span className="text-sm font-mono text-gray-500 mt-2">
{t("execution.message.no-connection")}
</span>
</div>
) : (
<>
<div className="w-full flex flex-row justify-start items-center mt-4">
<span className="opacity-70">{t("connection.self")}: </span>
<EngineIcon className="w-6 h-auto" engine={context.connection.engineType} />
<EngineIcon
className="w-6 h-auto"
engine={context.connection.engineType}
/>
<span>{context.database?.name}</span>
</div>
{showExecutionWarningBanner && <ExecutionWarningBanner className="rounded-lg mt-4" />}
{showExecutionWarningBanner && (
<ExecutionWarningBanner className="rounded-lg mt-4" />
)}
<div className="w-full h-auto mt-4 px-2 flex flex-row justify-between items-end border dark:border-zinc-700 rounded-lg overflow-clip">
<TextareaAutosize
className="w-full h-full outline-none border-none bg-transparent leading-6 pl-2 py-2 resize-none hide-scrollbar text-sm font-mono break-all whitespace-pre-wrap"
@ -126,15 +146,22 @@ const QueryDrawer = () => {
{isLoading ? (
<div className="w-full flex flex-col justify-center items-center py-6 pt-10">
<Icon.BiLoaderAlt className="w-7 h-auto opacity-70 animate-spin" />
<span className="text-sm font-mono text-gray-500 mt-2">{t("execution.message.executing")}</span>
<span className="text-sm font-mono text-gray-500 mt-2">
{t("execution.message.executing")}
</span>
</div>
) : (
<>
{executionResult ? (
executionMessage ? (
<NotificationView message={executionMessage} style={executionResult?.error ? "error" : "info"} />
<NotificationView
message={executionMessage}
style={executionResult?.error ? "error" : "info"}
/>
) : (
<DataTableView rawResults={executionResult?.rawResult || []} />
<DataTableView
rawResults={executionResult?.rawResult || []}
/>
)
) : (
<></>

View File

@ -11,7 +11,10 @@ interface Props {
const QuotaOverflowBanner = (props: Props) => {
const { className } = props;
const { t } = useTranslation();
const [hideBanner, setHideBanner] = useLocalStorage("hide-quota-overflow-banner", false);
const [hideBanner, setHideBanner] = useLocalStorage(
"hide-quota-overflow-banner",
false
);
const [showSettingModal, setShowSettingModal] = useState(false);
const show = !hideBanner;
@ -24,16 +27,24 @@ const QuotaOverflowBanner = (props: Props) => {
>
<div className="text-sm leading-6 pr-4 cursor-pointer">
{t("banner.quota-overflow")}{" "}
<button className="ml-1 underline hover:opacity-80" onClick={() => setShowSettingModal(true)}>
<button
className="ml-1 underline hover:opacity-80"
onClick={() => setShowSettingModal(true)}
>
{t("banner.use-my-key")}
</button>
</div>
<button className="absolute right-2 sm:right-4 opacity-60 hover:opacity-100" onClick={() => setHideBanner(true)}>
<button
className="absolute right-2 sm:right-4 opacity-60 hover:opacity-100"
onClick={() => setHideBanner(true)}
>
<Icon.BiX className="w-6 h-auto" />
</button>
</div>
{showSettingModal && <SettingModal close={() => setShowSettingModal(false)} />}
{showSettingModal && (
<SettingModal close={() => setShowSettingModal(false)} />
)}
</>
);
};

View File

@ -30,7 +30,9 @@ const SettingModal = (props: Props) => {
<WeChatQRCodeView />
</div>
<h3 className="pl-4 text-sm text-gray-500">{t("setting.basic.self")}</h3>
<h3 className="pl-4 text-sm text-gray-500">
{t("setting.basic.self")}
</h3>
<div className="w-full border border-gray-200 dark:border-zinc-700 p-4 rounded-lg space-y-2">
<div className="w-full flex flex-row justify-between items-center gap-2">
<span>{t("setting.basic.language")}</span>

View File

@ -22,7 +22,8 @@ const ConnectionList = () => {
showSettingModal: false,
showUpdateConversationModal: false,
});
const [editConnectionModalContext, setEditConnectionModalContext] = useState<Connection>();
const [editConnectionModalContext, setEditConnectionModalContext] =
useState<Connection>();
const connectionList = connectionStore.connectionList;
const currentConnectionCtx = connectionStore.currentConnectionCtx;
@ -35,7 +36,9 @@ const ConnectionList = () => {
};
const handleConnectionSelect = async (connection: Connection) => {
const databaseList = await connectionStore.getOrFetchDatabaseList(connection);
const databaseList = await connectionStore.getOrFetchDatabaseList(
connection
);
connectionStore.setCurrentConnectionCtx({
connection,
database: head(databaseList),
@ -53,7 +56,10 @@ const ConnectionList = () => {
return (
<>
<button
className={`w-full h-14 rounded-l-lg p-2 mt-1 group ${currentConnectionCtx === undefined && "bg-gray-100 dark:bg-zinc-700 shadow"}`}
className={`w-full h-14 rounded-l-lg p-2 mt-1 group ${
currentConnectionCtx === undefined &&
"bg-gray-100 dark:bg-zinc-700 shadow"
}`}
onClick={() => connectionStore.setCurrentConnectionCtx(undefined)}
>
<img src="/chat-logo-bot.webp" className="w-7 h-auto mx-auto" alt="" />
@ -62,7 +68,8 @@ const ConnectionList = () => {
<Tooltip key={connection.id} title={connection.title} side="right">
<button
className={`relative w-full h-14 rounded-l-lg p-2 mt-2 group ${
currentConnectionCtx?.connection.id === connection.id && "bg-gray-100 dark:bg-zinc-700 shadow"
currentConnectionCtx?.connection.id === connection.id &&
"bg-gray-100 dark:bg-zinc-700 shadow"
}`}
onClick={() => handleConnectionSelect(connection)}
>
@ -75,7 +82,10 @@ const ConnectionList = () => {
>
<Icon.FiEdit3 className="w-3.5 h-auto dark:text-gray-300" />
</span>
<EngineIcon engine={connection.engineType} className="w-auto h-full mx-auto dark:text-gray-300" />
<EngineIcon
engine={connection.engineType}
className="w-auto h-full mx-auto dark:text-gray-300"
/>
</button>
</Tooltip>
))}
@ -89,7 +99,10 @@ const ConnectionList = () => {
</Tooltip>
{state.showCreateConnectionModal && (
<CreateConnectionModal connection={editConnectionModalContext} close={() => toggleCreateConnectionModal(false)} />
<CreateConnectionModal
connection={editConnectionModalContext}
close={() => toggleCreateConnectionModal(false)}
/>
)}
</>
);

View File

@ -1,6 +1,10 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useConversationStore, useConnectionStore, useLayoutStore } from "@/store";
import {
useConversationStore,
useConnectionStore,
useLayoutStore,
} from "@/store";
import { Conversation } from "@/types";
import Dropdown, { DropdownItem } from "../kit/Dropdown";
import Icon from "../Icon";
@ -18,7 +22,8 @@ const ConversationList = () => {
const [state, setState] = useState<State>({
showUpdateConversationModal: false,
});
const [updateConversationModalContext, setUpdateConversationModalContext] = useState<Conversation>();
const [updateConversationModalContext, setUpdateConversationModalContext] =
useState<Conversation>();
const currentConnectionCtx = connectionStore.currentConnectionCtx;
const conversationList = conversationStore.conversationList.filter(
(conversation) =>
@ -37,7 +42,10 @@ const ConversationList = () => {
if (!currentConnectionCtx) {
conversationStore.createConversation();
} else {
conversationStore.createConversation(currentConnectionCtx.connection.id, currentConnectionCtx.database?.name);
conversationStore.createConversation(
currentConnectionCtx.connection.id,
currentConnectionCtx.database?.name
);
}
};
@ -69,7 +77,8 @@ const ConversationList = () => {
<div
key={conversation.id}
className={`w-full mt-2 first:mt-4 py-3 pl-4 pr-2 rounded-lg flex flex-row justify-start items-center cursor-pointer dark:text-gray-300 border border-transparent group hover:bg-white dark:hover:bg-zinc-800 ${
conversation.id === conversationStore.currentConversationId && "bg-white dark:bg-zinc-800 border-gray-200 font-medium"
conversation.id === conversationStore.currentConversationId &&
"bg-white dark:bg-zinc-800 border-gray-200 font-medium"
}`}
onClick={() => handleConversationSelect(conversation)}
>
@ -78,7 +87,9 @@ const ConversationList = () => {
) : (
<Icon.IoChatbubbleOutline className="w-5 h-auto mr-1.5 opacity-80 shrink-0" />
)}
<span className="truncate grow">{conversation.title || "SQL Chat"}</span>
<span className="truncate grow">
{conversation.title || "SQL Chat"}
</span>
<Dropdown
tigger={
<button className="w-4 h-4 shrink-0 group-hover:visible invisible flex justify-center items-center text-gray-400 hover:text-gray-500">
@ -114,7 +125,10 @@ const ConversationList = () => {
</button>
{updateConversationModalContext && state.showUpdateConversationModal && (
<UpdateConversationModal close={() => toggleUpdateConversationModal(false)} conversation={updateConversationModalContext} />
<UpdateConversationModal
close={() => toggleUpdateConversationModal(false)}
conversation={updateConversationModalContext}
/>
)}
</>
);

View File

@ -32,7 +32,14 @@ const ThemeSelector = () => {
settingStore.setTheme(theme);
};
return <Select className="w-auto min-w-[120px]" value={theme} itemList={themeItemList} onValueChange={handleThemeChange} />;
return (
<Select
className="w-auto min-w-[120px]"
value={theme}
itemList={themeItemList}
onValueChange={handleThemeChange}
/>
);
};
export default ThemeSelector;

View File

@ -14,7 +14,10 @@ const ThemeSwitch = () => {
};
return (
<button className="w-10 h-10 p-1 rounded-full flex flex-row justify-center items-center hover:bg-gray-100" onClick={handleThemeChange}>
<button
className="w-10 h-10 p-1 rounded-full flex flex-row justify-center items-center hover:bg-gray-100"
onClick={handleThemeChange}
>
<Icon.IoSunny className="text-gray-600 w-6 h-auto" />
</button>
);

View File

@ -27,7 +27,9 @@ const UpdateConversationModal = (props: Props) => {
label: assistant.name,
};
});
const currentAssistant = assistantList.find((assistant) => assistant.id === assistantId);
const currentAssistant = assistantList.find(
(assistant) => assistant.id === assistantId
);
const handleSaveEdit = () => {
const formatedTitle = title.trim();
@ -46,17 +48,30 @@ const UpdateConversationModal = (props: Props) => {
return (
<Modal title={t("conversation.update")} onClose={close}>
<div className="w-full flex flex-col justify-start items-start mt-2">
<label className="block text-sm font-medium text-gray-700 mb-1">{t("conversation.title")}</label>
<TextField placeholder={t("conversation.conversation-title") || ""} value={title} onChange={(value) => setTitle(value)} />
<label className="block text-sm font-medium text-gray-700 mb-1">
{t("conversation.title")}
</label>
<TextField
placeholder={t("conversation.conversation-title") || ""}
value={title}
onChange={(value) => setTitle(value)}
/>
</div>
<div className="w-full flex flex-col justify-start items-start mt-2">
<label className="text-sm font-medium text-gray-700 mb-1 flex flex-row justify-start items-center">
{t("assistant.self")} <BetaBadge />
</label>
<Select className="w-full" value={assistantId} itemList={assistantItems} onValueChange={(value) => setAssistantId(value)} />
<Select
className="w-full"
value={assistantId}
itemList={assistantItems}
onValueChange={(value) => setAssistantId(value)}
/>
{currentAssistant && (
<div className="w-full flex flex-col justify-start items-start">
<p className="block text-sm text-gray-700 mt-2 mx-3">{currentAssistant.description}</p>
<p className="block text-sm text-gray-700 mt-2 mx-3">
{currentAssistant.description}
</p>
</div>
)}
<a
@ -64,7 +79,8 @@ const UpdateConversationModal = (props: Props) => {
href="https://github.com/sqlchat/sqlchat/tree/main/assistants"
target="_blank"
>
{t("assistant.create-your-bot")} <Icon.FiExternalLink className="inline-block -mt-0.5" />
{t("assistant.create-your-bot")}{" "}
<Icon.FiExternalLink className="inline-block -mt-0.5" />
</a>
</div>
<div className="w-full flex flex-row justify-end items-center mt-4 space-x-2">

View File

@ -19,7 +19,9 @@ const Modal = (props: Props) => {
className || ""
} flex flex-col bg-white dark:bg-zinc-800 rounded-xl p-4 fixed top-[50%] left-[50%] h-auto max-h-[85vh] w-[90vw] max-w-[90vw] sm:max-w-lg translate-x-[-50%] translate-y-[-50%] z-100 outline-none`}
>
<p className="text-lg pl-1 text-black dark:text-gray-300 font-medium mb-2">{title}</p>
<p className="text-lg pl-1 text-black dark:text-gray-300 font-medium mb-2">
{title}
</p>
<button
className="absolute top-3 right-3 outline-none w-8 h-8 p-1 bg-zinc-600 rounded-full text-gray-300 hover:opacity-80"
aria-label="Close"
@ -27,7 +29,9 @@ const Modal = (props: Props) => {
>
<Icon.IoClose className="w-full h-auto" />
</button>
<div className="w-full px-1 h-[calc(100%-36px)] flex flex-col justify-start items-start overflow-y-auto">{children}</div>
<div className="w-full px-1 h-[calc(100%-36px)] flex flex-col justify-start items-start overflow-y-auto">
{children}
</div>
</div>
</ModalUI>
);

View File

@ -16,7 +16,9 @@ const Popover = (props: Props) => {
<PopoverUI.Portal>
<PopoverUI.Content
asChild
className={`${className || ""} z-[999] p-2 bg-white dark:bg-zinc-700 drop-shadow rounded-lg`}
className={`${
className || ""
} z-[999] p-2 bg-white dark:bg-zinc-700 drop-shadow rounded-lg`}
sideOffset={5}
>
{children}

View File

@ -1,5 +1,5 @@
import * as SelectUI from "@radix-ui/react-select";
import * as ScrollArea from '@radix-ui/react-scroll-area';
import * as ScrollArea from "@radix-ui/react-scroll-area";
import React from "react";
import Icon from "../Icon";
@ -37,18 +37,34 @@ const Select = (props: Props) => {
}}
position="popper"
>
<ScrollArea.Root className={`${itemList.length > 7 ? "h-80 overflow-hidden" : "max-h-80 overflow-auto"} border dark:border-zinc-800 rounded-lg drop-shadow-lg`} type="auto">
<SelectUI.Viewport asChild className="bg-white dark:bg-zinc-700 p-1 rounded-lg">
<ScrollArea.Root
className={`${
itemList.length > 7
? "h-80 overflow-hidden"
: "max-h-80 overflow-auto"
} border dark:border-zinc-800 rounded-lg drop-shadow-lg`}
type="auto"
>
<SelectUI.Viewport
asChild
className="bg-white dark:bg-zinc-700 p-1 rounded-lg"
>
<ScrollArea.Viewport className="w-full h-full">
<SelectUI.Group>
{placeholder && <SelectUI.Label className="w-full px-3 mt-2 mb-2 text-sm text-gray-400">{placeholder}</SelectUI.Label>}
<SelectUI.Group>
{placeholder && (
<SelectUI.Label className="w-full px-3 mt-2 mb-2 text-sm text-gray-400">
{placeholder}
</SelectUI.Label>
)}
{itemList.map((item) => (
<SelectUI.Item
className="w-full px-3 py-2 whitespace-nowrap truncate text-ellipsis overflow-x-hidden text-sm rounded-lg flex flex-row justify-between items-center cursor-pointer hover:bg-gray-100 dark:hover:bg-zinc-800"
key={item.label}
value={item.value}
>
<SelectUI.ItemText className="truncate">{item.label}</SelectUI.ItemText>
<SelectUI.ItemText className="truncate">
{item.label}
</SelectUI.ItemText>
<SelectUI.ItemIndicator className="w-5 h-auto">
<Icon.BiCheck className="w-full h-auto" />
</SelectUI.ItemIndicator>

View File

@ -19,11 +19,16 @@ const getDefaultProps = () => ({
});
const TextField = (props: Props) => {
const { value, disabled, className, placeholder, type, onChange } = { ...getDefaultProps(), ...props };
const { value, disabled, className, placeholder, type, onChange } = {
...getDefaultProps(),
...props,
};
return (
<input
className={`${className || ""} w-full border px-3 py-2 rounded-lg dark:border-zinc-700 dark:bg-zinc-800 focus:outline-2`}
className={`${
className || ""
} w-full border px-3 py-2 rounded-lg dark:border-zinc-700 dark:bg-zinc-800 focus:outline-2`}
type={type}
disabled={disabled}
placeholder={placeholder}

View File

@ -5,10 +5,17 @@ import mssql from "./mssql";
export interface Connector {
testConnection: () => Promise<boolean>;
execute: (databaseName: string, statement: string) => Promise<ExecutionResult>;
execute: (
databaseName: string,
statement: string
) => Promise<ExecutionResult>;
getDatabases: () => Promise<string[]>;
getTables: (databaseName: string) => Promise<string[]>;
getTableStructure: (databaseName: string, tableName: string, structureFetched: (tableName: string, structure: string) => void) => Promise<void>;
getTableStructure: (
databaseName: string,
tableName: string,
structureFetched: (tableName: string, structure: string) => void
) => Promise<void>;
}
export const newConnector = (connection: Connection): Connector => {

View File

@ -4,7 +4,9 @@ import { Connector } from "..";
const systemDatabases = ["master", "tempdb", "model", "msdb"];
const getMSSQLConnection = async (connection: Connection): Promise<ConnectionPool> => {
const getMSSQLConnection = async (
connection: Connection
): Promise<ConnectionPool> => {
const connectionOptions: any = {
server: connection.host,
port: parseInt(connection.port),
@ -32,7 +34,11 @@ const testConnection = async (connection: Connection): Promise<boolean> => {
return true;
};
const execute = async (connection: Connection, databaseName: string, statement: string): Promise<any> => {
const execute = async (
connection: Connection,
databaseName: string,
statement: string
): Promise<any> => {
const pool = await getMSSQLConnection(connection);
const request = pool.request();
const result = await request.query(`USE ${databaseName}; ${statement}`);
@ -48,7 +54,11 @@ const execute = async (connection: Connection, databaseName: string, statement:
const getDatabases = async (connection: Connection): Promise<string[]> => {
const pool = await getMSSQLConnection(connection);
const request = pool.request();
const result = await request.query(`SELECT name FROM sys.databases WHERE name NOT IN ('${systemDatabases.join("','")}');`);
const result = await request.query(
`SELECT name FROM sys.databases WHERE name NOT IN ('${systemDatabases.join(
"','"
)}');`
);
await pool.close();
const databaseList = [];
for (const row of result.recordset) {
@ -59,7 +69,10 @@ const getDatabases = async (connection: Connection): Promise<string[]> => {
return databaseList;
};
const getTables = async (connection: Connection, databaseName: string): Promise<string[]> => {
const getTables = async (
connection: Connection,
databaseName: string
): Promise<string[]> => {
const pool = await getMSSQLConnection(connection);
const request = pool.request();
const result = await request.query(
@ -75,7 +88,12 @@ const getTables = async (connection: Connection, databaseName: string): Promise<
return tableList;
};
const getTableStructure = async (connection: Connection, databaseName: string, tableName: string, structureFetched: (tableName: string,structure: string) => void): Promise<void> => {
const getTableStructure = async (
connection: Connection,
databaseName: string,
tableName: string,
structureFetched: (tableName: string, structure: string) => void
): Promise<void> => {
const pool = await getMSSQLConnection(connection);
const request = pool.request();
const { recordset } = await request.query(
@ -86,21 +104,32 @@ const getTableStructure = async (connection: Connection, databaseName: string, t
// Transform to standard schema string.
for (const row of recordset) {
columnList.push(
`${row["COLUMN_NAME"]} ${row["DATA_TYPE"].toUpperCase()} ${String(row["IS_NULLABLE"]).toUpperCase() === "NO" ? "NOT NULL" : ""}`
`${row["COLUMN_NAME"]} ${row["DATA_TYPE"].toUpperCase()} ${
String(row["IS_NULLABLE"]).toUpperCase() === "NO" ? "NOT NULL" : ""
}`
);
}
structureFetched(tableName, `CREATE TABLE [${tableName}] (
structureFetched(
tableName,
`CREATE TABLE [${tableName}] (
${columnList.join(",\n")}
);`);
);`
);
};
const newConnector = (connection: Connection): Connector => {
return {
testConnection: () => testConnection(connection),
execute: (databaseName: string, statement: string) => execute(connection, databaseName, statement),
execute: (databaseName: string, statement: string) =>
execute(connection, databaseName, statement),
getDatabases: () => getDatabases(connection),
getTables: (databaseName: string) => getTables(connection, databaseName),
getTableStructure: (databaseName: string, tableName: string, structureFetched: (tableName: string, structure: string) => void) => getTableStructure(connection, databaseName, tableName, structureFetched),
getTableStructure: (
databaseName: string,
tableName: string,
structureFetched: (tableName: string, structure: string) => void
) =>
getTableStructure(connection, databaseName, tableName, structureFetched),
};
};

View File

@ -3,9 +3,16 @@ import mysql, { RowDataPacket } from "mysql2/promise";
import { Connection, ExecutionResult } from "@/types";
import { Connector } from "..";
const systemDatabases = ["information_schema", "mysql", "performance_schema", "sys"];
const systemDatabases = [
"information_schema",
"mysql",
"performance_schema",
"sys",
];
const getMySQLConnection = async (connection: Connection): Promise<mysql.Connection> => {
const getMySQLConnection = async (
connection: Connection
): Promise<mysql.Connection> => {
const connectionOptions: ConnectionOptions = {
host: connection.host,
port: parseInt(connection.port),
@ -30,7 +37,11 @@ const testConnection = async (connection: Connection): Promise<boolean> => {
return true;
};
const execute = async (connection: Connection, databaseName: string, statement: string): Promise<any> => {
const execute = async (
connection: Connection,
databaseName: string,
statement: string
): Promise<any> => {
connection.database = databaseName;
const conn = await getMySQLConnection(connection);
const [rows] = await conn.execute(statement);
@ -64,7 +75,10 @@ const getDatabases = async (connection: Connection): Promise<string[]> => {
return databaseList;
};
const getTables = async (connection: Connection, databaseName: string): Promise<string[]> => {
const getTables = async (
connection: Connection,
databaseName: string
): Promise<string[]> => {
const conn = await getMySQLConnection(connection);
const [rows] = await conn.query<RowDataPacket[]>(
`SELECT TABLE_NAME as table_name FROM information_schema.tables WHERE TABLE_SCHEMA=? AND TABLE_TYPE='BASE TABLE';`,
@ -80,9 +94,16 @@ const getTables = async (connection: Connection, databaseName: string): Promise<
return tableList;
};
const getTableStructure = async (connection: Connection, databaseName: string, tableName: string, structureFetched: (tableName: string,structure: string) => void): Promise<void> => {
const getTableStructure = async (
connection: Connection,
databaseName: string,
tableName: string,
structureFetched: (tableName: string, structure: string) => void
): Promise<void> => {
const conn = await getMySQLConnection(connection);
const [rows] = await conn.query<RowDataPacket[]>(`SHOW CREATE TABLE \`${databaseName}\`.\`${tableName}\`;`);
const [rows] = await conn.query<RowDataPacket[]>(
`SHOW CREATE TABLE \`${databaseName}\`.\`${tableName}\`;`
);
conn.destroy();
if (rows.length !== 1) {
throw new Error("Unexpected number of rows.");
@ -93,10 +114,16 @@ const getTableStructure = async (connection: Connection, databaseName: string, t
const newConnector = (connection: Connection): Connector => {
return {
testConnection: () => testConnection(connection),
execute: (databaseName: string, statement: string) => execute(connection, databaseName, statement),
execute: (databaseName: string, statement: string) =>
execute(connection, databaseName, statement),
getDatabases: () => getDatabases(connection),
getTables: (databaseName: string) => getTables(connection, databaseName),
getTableStructure: (databaseName: string, tableName: string, structureFetched: (tableName: string, structure: string) => void) => getTableStructure(connection, databaseName, tableName, structureFetched),
getTableStructure: (
databaseName: string,
tableName: string,
structureFetched: (tableName: string, structure: string) => void
) =>
getTableStructure(connection, databaseName, tableName, structureFetched),
};
};

View File

@ -27,7 +27,11 @@ const testConnection = async (connection: Connection): Promise<boolean> => {
return true;
};
const execute = async (connection: Connection, databaseName: string, statement: string): Promise<any> => {
const execute = async (
connection: Connection,
databaseName: string,
statement: string
): Promise<any> => {
connection.database = databaseName;
const client = newPostgresClient(connection);
await client.connect();
@ -65,7 +69,10 @@ const getDatabases = async (connection: Connection): Promise<string[]> => {
return databaseList;
};
const getTables = async (connection: Connection, databaseName: string): Promise<string[]> => {
const getTables = async (
connection: Connection,
databaseName: string
): Promise<string[]> => {
connection.database = databaseName;
const client = newPostgresClient(connection);
await client.connect();
@ -83,7 +90,12 @@ const getTables = async (connection: Connection, databaseName: string): Promise<
return tableList;
};
const getTableStructure = async (connection: Connection, databaseName: string, tableName: string, structureFetched: (tableName: string,structure: string) => void): Promise<void> => {
const getTableStructure = async (
connection: Connection,
databaseName: string,
tableName: string,
structureFetched: (tableName: string, structure: string) => void
): Promise<void> => {
connection.database = databaseName;
const client = newPostgresClient(connection);
await client.connect();
@ -96,21 +108,32 @@ const getTableStructure = async (connection: Connection, databaseName: string, t
// TODO(steven): transform it to standard schema string.
for (const row of rows) {
columnList.push(
`${row["column_name"]} ${row["data_type"].toUpperCase()} ${String(row["is_nullable"]).toUpperCase() === "NO" ? "NOT NULL" : ""}`
`${row["column_name"]} ${row["data_type"].toUpperCase()} ${
String(row["is_nullable"]).toUpperCase() === "NO" ? "NOT NULL" : ""
}`
);
}
structureFetched(tableName, `CREATE TABLE \`${tableName}\` (
structureFetched(
tableName,
`CREATE TABLE \`${tableName}\` (
${columnList.join(",\n")}
);`);
);`
);
};
const newConnector = (connection: Connection): Connector => {
return {
testConnection: () => testConnection(connection),
execute: (databaseName: string, statement: string) => execute(connection, databaseName, statement),
execute: (databaseName: string, statement: string) =>
execute(connection, databaseName, statement),
getDatabases: () => getDatabases(connection),
getTables: (databaseName: string) => getTables(connection, databaseName),
getTableStructure: (databaseName: string, tableName: string, structureFetched: (tableName: string, structure: string) => void) => getTableStructure(connection, databaseName, tableName, structureFetched),
getTableStructure: (
databaseName: string,
tableName: string,
structureFetched: (tableName: string, structure: string) => void
) =>
getTableStructure(connection, databaseName, tableName, structureFetched),
};
};

View File

@ -52,7 +52,10 @@ function MyApp({ Component, pageProps }: AppProps) {
const theme = settingStore.setting.theme;
let currentAppearance = theme;
if (theme === "system") {
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
if (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
currentAppearance = "dark";
} else {
currentAppearance = "light";

View File

@ -1,4 +1,8 @@
import { createParser, ParsedEvent, ReconnectInterval } from "eventsource-parser";
import {
createParser,
ParsedEvent,
ReconnectInterval,
} from "eventsource-parser";
import { NextRequest } from "next/server";
import { API_KEY } from "@/env";
import { openAIApiEndpoint, openAIApiKey } from "@/utils";
@ -26,7 +30,9 @@ const handler = async (req: NextRequest) => {
const reqBody = await req.json();
const openAIApiConfig = reqBody.openAIApiConfig;
const apiKey = openAIApiConfig?.key || openAIApiKey;
const apiEndpoint = getApiEndpoint(openAIApiConfig?.endpoint || openAIApiEndpoint);
const apiEndpoint = getApiEndpoint(
openAIApiConfig?.endpoint || openAIApiEndpoint
);
const res = await fetch(apiEndpoint, {
headers: {
"Content-Type": "application/json",

View File

@ -16,18 +16,20 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const connector = newConnector(connection);
const tableStructures: Table[] = [];
const rawTableNameList = await connector.getTables(db);
const structureFetched = (tableName:string, structure:string) => {
const structureFetched = (tableName: string, structure: string) => {
tableStructures.push({
name: tableName,
structure,
});
}
Promise.all(rawTableNameList.map(async (tableName) =>
connector.getTableStructure(db, tableName, structureFetched)
)).then(() => {
};
Promise.all(
rawTableNameList.map(async (tableName) =>
connector.getTableStructure(db, tableName, structureFetched)
)
).then(() => {
res.status(200).json({
data: tableStructures,
});
});
});
} catch (error: any) {
res.status(400).json({

View File

@ -4,7 +4,10 @@ import { Conversation, Message } from "@/types";
const prisma = new PrismaClient();
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
return res.status(405).json([]);
}

View File

@ -6,12 +6,18 @@ import React from "react";
// Use dynamic import to avoid page hydrated.
// reference: https://github.com/pmndrs/zustand/issues/1145#issuecomment-1316431268
const ConnectionSidebar = dynamic(() => import("@/components/ConnectionSidebar"), {
ssr: false,
});
const ConversationView = dynamic(() => import("@/components/ConversationView"), {
ssr: false,
});
const ConnectionSidebar = dynamic(
() => import("@/components/ConnectionSidebar"),
{
ssr: false,
}
);
const ConversationView = dynamic(
() => import("@/components/ConversationView"),
{
ssr: false,
}
);
const QueryDrawer = dynamic(() => import("@/components/QueryDrawer"), {
ssr: false,
});
@ -20,14 +26,31 @@ const IndexPage: NextPage = () => {
return (
<div>
<Head>
<title>SQL Chat - Chat-based SQL Client and Editor for the next decade</title>
<meta name="description" content="Chat-based SQL Client and Editor for the next decade" />
<title>
SQL Chat - Chat-based SQL Client and Editor for the next decade
</title>
<meta
name="description"
content="Chat-based SQL Client and Editor for the next decade"
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="og:title" property="og:title" content="SQL Chat" />
<meta name="og:description" property="og:description" content="Chat-based SQL Client and Editor for the next decade" />
<meta name="og:image" property="og:image" content="https://www.sqlchat.ai/chat-logo-and-text.webp" />
<meta
name="og:description"
property="og:description"
content="Chat-based SQL Client and Editor for the next decade"
/>
<meta
name="og:image"
property="og:image"
content="https://www.sqlchat.ai/chat-logo-and-text.webp"
/>
<meta name="og:type" property="og:type" content="website" />
<meta name="og:url" property="og:url" content="https://www.sqlchat.ai" />
<meta
name="og:url"
property="og:url"
content="https://www.sqlchat.ai"
/>
</Head>
<h1 className="sr-only">SQL Chat</h1>
@ -38,7 +61,11 @@ const IndexPage: NextPage = () => {
<QueryDrawer />
</main>
<Script defer data-domain="sqlchat.ai" src="https://plausible.io/js/script.js" />
<Script
defer
data-domain="sqlchat.ai"
src="https://plausible.io/js/script.js"
/>
</div>
);
};

View File

@ -5,11 +5,13 @@ import * as customAssistantList from "../../assistants";
export const GeneralBotId = "general-bot";
export const SQLChatBotId = "sql-chat-bot";
export const assistantList: Assistant[] = Object.keys(customAssistantList).map((name) => {
return {
...((customAssistantList as any)[name].default as Assistant),
};
});
export const assistantList: Assistant[] = Object.keys(customAssistantList).map(
(name) => {
return {
...((customAssistantList as any)[name].default as Assistant),
};
}
);
export const getAssistantById = (id: Id) => {
const assistant = assistantList.find((assistant) => assistant.id === id);

View File

@ -26,11 +26,22 @@ interface ConnectionState {
databaseList: Database[];
currentConnectionCtx?: ConnectionContext;
createConnection: (connection: Connection) => Connection;
setCurrentConnectionCtx: (connectionCtx: ConnectionContext | undefined) => void;
getOrFetchDatabaseList: (connection: Connection, skipCache?: boolean) => Promise<Database[]>;
getOrFetchDatabaseSchema: (database: Database, skipCache?: boolean) => Promise<Table[]>;
setCurrentConnectionCtx: (
connectionCtx: ConnectionContext | undefined
) => void;
getOrFetchDatabaseList: (
connection: Connection,
skipCache?: boolean
) => Promise<Database[]>;
getOrFetchDatabaseSchema: (
database: Database,
skipCache?: boolean
) => Promise<Table[]>;
getConnectionById: (connectionId: string) => Connection | undefined;
updateConnection: (connectionId: string, connection: Partial<Connection>) => void;
updateConnection: (
connectionId: string,
connection: Partial<Connection>
) => void;
clearConnection: (filter: (connection: Connection) => boolean) => void;
}
@ -55,12 +66,21 @@ export const useConnectionStore = create<ConnectionState>()(
...state,
currentConnectionCtx: connectionCtx,
})),
getOrFetchDatabaseList: async (connection: Connection, skipCache = false) => {
getOrFetchDatabaseList: async (
connection: Connection,
skipCache = false
) => {
const state = get();
if (!skipCache) {
if (state.databaseList.some((database) => database.connectionId === connection.id)) {
return state.databaseList.filter((database) => database.connectionId === connection.id);
if (
state.databaseList.some(
(database) => database.connectionId === connection.id
)
) {
return state.databaseList.filter(
(database) => database.connectionId === connection.id
);
}
}
@ -83,27 +103,45 @@ export const useConnectionStore = create<ConnectionState>()(
...state,
databaseList,
}));
return databaseList.filter((database) => database.connectionId === connection.id);
return databaseList.filter(
(database) => database.connectionId === connection.id
);
},
getOrFetchDatabaseSchema: async (database: Database, skipCache = false) => {
getOrFetchDatabaseSchema: async (
database: Database,
skipCache = false
) => {
const state = get();
if (!skipCache) {
const db = state.databaseList.find((db) => db.connectionId === database.connectionId && db.name === database.name);
if (db !== undefined && Array.isArray(db.tableList) && db.tableList.length !== 0) {
const db = state.databaseList.find(
(db) =>
db.connectionId === database.connectionId &&
db.name === database.name
);
if (
db !== undefined &&
Array.isArray(db.tableList) &&
db.tableList.length !== 0
) {
return db.tableList;
}
}
const connection = state.connectionList.find((connection) => connection.id === database.connectionId);
const connection = state.connectionList.find(
(connection) => connection.id === database.connectionId
);
if (!connection) {
return [];
}
const { data: result } = await axios.post<ResponseObject<Table[]>>("/api/connection/db_schema", {
connection,
db: database.name,
});
const { data: result } = await axios.post<ResponseObject<Table[]>>(
"/api/connection/db_schema",
{
connection,
db: database.name,
}
);
if (result.message) {
throw result.message;
}
@ -112,19 +150,29 @@ export const useConnectionStore = create<ConnectionState>()(
set((state) => ({
...state,
databaseList: state.databaseList.map((item) =>
item.connectionId === database.connectionId && item.name === database.name ? { ...item, tableList: fetchedTableList } : item
item.connectionId === database.connectionId &&
item.name === database.name
? { ...item, tableList: fetchedTableList }
: item
),
}));
return fetchedTableList;
},
getConnectionById: (connectionId: string) => {
return get().connectionList.find((connection) => connection.id === connectionId);
return get().connectionList.find(
(connection) => connection.id === connectionId
);
},
updateConnection: (connectionId: string, connection: Partial<Connection>) => {
updateConnection: (
connectionId: string,
connection: Partial<Connection>
) => {
set((state) => ({
...state,
connectionList: state.connectionList.map((item) => (item.id === connectionId ? { ...item, ...connection } : item)),
connectionList: state.connectionList.map((item) =>
item.id === connectionId ? { ...item, ...connection } : item
),
}));
},
clearConnection: (filter: (connection: Connection) => boolean) => {

View File

@ -18,10 +18,18 @@ interface ConversationState {
getState: () => ConversationState;
conversationList: Conversation[];
currentConversationId?: Id;
createConversation: (connectionId?: Id, databaseName?: string) => Conversation;
createConversation: (
connectionId?: Id,
databaseName?: string
) => Conversation;
setCurrentConversationId: (conversationId: Id | undefined) => void;
getConversationById: (conversationId: Id | undefined) => Conversation | undefined;
updateConversation: (conversationId: Id, conversation: Partial<Conversation>) => void;
getConversationById: (
conversationId: Id | undefined
) => Conversation | undefined;
updateConversation: (
conversationId: Id,
conversation: Partial<Conversation>
) => void;
clearConversation: (filter: (conversation: Conversation) => boolean) => void;
}
@ -45,14 +53,22 @@ export const useConversationStore = create<ConversationState>()(
}));
return conversation;
},
setCurrentConversationId: (conversation: Id | undefined) => set(() => ({ currentConversationId: conversation })),
setCurrentConversationId: (conversation: Id | undefined) =>
set(() => ({ currentConversationId: conversation })),
getConversationById: (conversationId: Id | undefined) => {
return get().conversationList.find((item) => item.id === conversationId);
return get().conversationList.find(
(item) => item.id === conversationId
);
},
updateConversation: (conversationId: Id, conversation: Partial<Conversation>) => {
updateConversation: (
conversationId: Id,
conversation: Partial<Conversation>
) => {
set((state) => ({
...state,
conversationList: state.conversationList.map((item) => (item.id === conversationId ? { ...item, ...conversation } : item)),
conversationList: state.conversationList.map((item) =>
item.id === conversationId ? { ...item, ...conversation } : item
),
}));
},
clearConversation: (filter: (conversation: Conversation) => boolean) => {

View File

@ -15,14 +15,18 @@ export const useMessageStore = create<MessageState>()(
(set, get) => ({
messageList: [],
getState: () => get(),
addMessage: (message: Message) => set((state) => ({ messageList: [...state.messageList, message] })),
addMessage: (message: Message) =>
set((state) => ({ messageList: [...state.messageList, message] })),
updateMessage: (messageId: Id, message: Partial<Message>) => {
set((state) => ({
...state,
messageList: state.messageList.map((item) => (item.id === messageId ? { ...item, ...message } : item)),
messageList: state.messageList.map((item) =>
item.id === messageId ? { ...item, ...message } : item
),
}));
},
clearMessage: (filter: (message: Message) => boolean) => set((state) => ({ messageList: state.messageList.filter(filter) })),
clearMessage: (filter: (message: Message) => boolean) =>
set((state) => ({ messageList: state.messageList.filter(filter) })),
}),
{
name: "message-storage",

View File

@ -54,7 +54,8 @@ export const useSettingStore = create<SettingState>()(
}),
{
name: "setting-storage",
merge: (persistedState, currentState) => merge(currentState, persistedState),
merge: (persistedState, currentState) =>
merge(currentState, persistedState),
}
)
);

View File

@ -1,6 +1,8 @@
import { ExecutionResult } from "@/types";
export const getMessageFromExecutionResult = (result: ExecutionResult): string => {
export const getMessageFromExecutionResult = (
result: ExecutionResult
): string => {
if (result.error) {
return result.error;
}

View File

@ -4,7 +4,8 @@ import { encode } from "@nem035/gpt-3-encoder";
export const openAIApiKey = process.env.OPENAI_API_KEY;
// openAIApiEndpoint is the API endpoint for OpenAI API. Defaults to https://api.openai.com.
export const openAIApiEndpoint = process.env.OPENAI_API_ENDPOINT || "https://api.openai.com";
export const openAIApiEndpoint =
process.env.OPENAI_API_ENDPOINT || "https://api.openai.com";
export const countTextTokens = (text: string) => {
return encode(text).length;

View File

@ -1,6 +1,9 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/pages/**/*.{js,ts,jsx,tsx}", "./src/components/**/*.{js,ts,jsx,tsx}"],
content: [
"./src/pages/**/*.{js,ts,jsx,tsx}",
"./src/components/**/*.{js,ts,jsx,tsx}",
],
darkMode: "class",
theme: {
extend: {