Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9444b66a1 | ||
|
|
ec561fcd7f | ||
|
|
ee2b3e4fb2 | ||
|
|
67716f0d2d | ||
|
|
e56d90e45a | ||
|
|
92eee52c52 | ||
|
|
f4d995be4c | ||
|
|
fbdfbdd620 | ||
|
|
fec733e10b | ||
|
|
23905dd344 | ||
|
|
ec13d74b84 | ||
|
|
231906161b | ||
|
|
5c787035e5 | ||
|
|
2694690ed0 | ||
|
|
562bf8c920 | ||
|
|
2781154df3 | ||
|
|
691b6d9029 | ||
|
|
c6c3054c22 | ||
|
|
d71b61ad71 | ||
|
|
47533736e3 | ||
|
|
a17b878617 | ||
|
|
91ef4872d6 | ||
|
|
c1ddd07166 | ||
|
|
7fdc862042 | ||
|
|
8d75b25104 | ||
|
|
26152d7e5f | ||
|
|
61a4231feb | ||
|
|
c9b035a0bd | ||
|
|
44ea3601c9 | ||
|
|
782a899ab3 | ||
|
|
14104b276f | ||
|
|
08f3a77d58 | ||
|
|
ca26732cb8 | ||
|
|
dbf45196ee | ||
|
|
45a2aaf7b8 | ||
|
|
c02c62f3b1 | ||
|
|
4718674688 | ||
|
|
0e3c115368 | ||
|
|
cc506c23af | ||
|
|
1f77d94b7e | ||
|
|
5711ff27ee | ||
|
|
3120602d6a | ||
|
|
9f36e195bc | ||
|
|
9de7da91a7 | ||
|
|
501a15a18f | ||
|
|
6049c9e3ff | ||
|
|
262b402606 | ||
|
|
56ea9563b8 | ||
|
|
2cd6612620 | ||
|
|
5d40396fb2 | ||
|
|
93dd1eb036 | ||
|
|
542a46dc7c | ||
|
|
bf31b1fea0 | ||
|
|
25d4529ff9 | ||
|
|
33d7c67c04 | ||
|
|
dc8f762bac | ||
|
|
49041e16c7 | ||
|
|
3414690e42 | ||
|
|
95c97561ae | ||
|
|
8bb4d7d590 | ||
|
|
94ad31dce3 | ||
|
|
91e9b167b3 | ||
|
|
53ea3dd9fb | ||
|
|
c72a3a0362 | ||
|
|
bd068c9a5a | ||
|
|
7997c3137a | ||
|
|
b466b36e7a |
@@ -1,2 +1,3 @@
|
||||
**/node_modules
|
||||
**/.env
|
||||
api/.env
|
||||
client/dist/images
|
||||
114
.eslintrc.js
Normal file
114
.eslintrc.js
Normal file
@@ -0,0 +1,114 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
commonjs: true,
|
||||
es6: true
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
plugins: ['react', 'react-hooks', '@typescript-eslint'],
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }],
|
||||
indent: ['error', 2, { SwitchCase: 1 }],
|
||||
'max-len': [
|
||||
'error',
|
||||
{
|
||||
code: 150,
|
||||
ignoreStrings: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
ignoreComments: true
|
||||
}
|
||||
],
|
||||
'linebreak-style': 0,
|
||||
// "arrow-parens": [2, "as-needed", { requireForBlockBody: true }],
|
||||
// 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
|
||||
'no-console': 'off',
|
||||
'import/extensions': 'off',
|
||||
'no-use-before-define': [
|
||||
'error',
|
||||
{
|
||||
functions: false
|
||||
}
|
||||
],
|
||||
'no-promise-executor-return': 'off',
|
||||
'no-param-reassign': 'off',
|
||||
'no-continue': 'off',
|
||||
'no-restricted-syntax': 'off',
|
||||
'react/prop-types': ['off'],
|
||||
'react/display-name': ['off']
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
rules: {
|
||||
'no-unused-vars': 'off', // off because it conflicts with '@typescript-eslint/no-unused-vars'
|
||||
'react/display-name': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'warn'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['rollup.config.js', '.eslintrc.js', 'jest.config.js'],
|
||||
env: {
|
||||
node: true
|
||||
}
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'**/*.test.js',
|
||||
'**/*.test.jsx',
|
||||
'**/*.test.ts',
|
||||
'**/*.test.tsx',
|
||||
'**/*.spec.js',
|
||||
'**/*.spec.jsx',
|
||||
'**/*.spec.ts',
|
||||
'**/*.spec.tsx',
|
||||
'setupTests.js'
|
||||
],
|
||||
env: {
|
||||
jest: true,
|
||||
node: true
|
||||
},
|
||||
rules: {
|
||||
'react/display-name': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/no-unescaped-entities': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: '**/*.+(ts)',
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: './client/tsconfig.json'
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended'
|
||||
]
|
||||
}
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
createClass: 'createReactClass', // Regex for Component Factory to use,
|
||||
// default to "createReactClass"
|
||||
pragma: 'React', // Pragma to use, default to "React"
|
||||
fragment: 'Fragment', // Fragment to use (may be a property of <pragma>), default to "Fragment"
|
||||
version: 'detect' // React version. "detect" automatically picks the version you have installed.
|
||||
}
|
||||
}
|
||||
};
|
||||
41
.github/dependabot.yml
vendored
Normal file
41
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/api" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
allow:
|
||||
# Allow both direct and indirect updates for all packages
|
||||
- dependency-type: "all"
|
||||
commit-message:
|
||||
prefix: "npm api prod"
|
||||
prefix-development: "npm api dev"
|
||||
include: "scope"
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/client" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
allow:
|
||||
# Allow both direct and indirect updates for all packages
|
||||
- dependency-type: "all"
|
||||
commit-message:
|
||||
prefix: "npm client prod"
|
||||
prefix-development: "npm client dev"
|
||||
include: "scope"
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
allow:
|
||||
# Allow both direct and indirect updates for all packages
|
||||
- dependency-type: "all"
|
||||
commit-message:
|
||||
prefix: "npm all prod"
|
||||
prefix-development: "npm all dev"
|
||||
include: "scope"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -59,6 +59,7 @@ src/style - official.css
|
||||
/playwright/.cache/
|
||||
.DS_Store
|
||||
*.code-workspace
|
||||
.idea
|
||||
|
||||
# meilisearch
|
||||
meilisearch
|
||||
|
||||
5
.husky/pre-commit
Executable file
5
.husky/pre-commit
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
|
||||
19
.prettierrc.js
Normal file
19
.prettierrc.js
Normal file
@@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
useTabs: false,
|
||||
tabWidth: 2,
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
// bracketSpacing: false,
|
||||
trailingComma: 'none',
|
||||
arrowParens: 'always',
|
||||
embeddedLanguageFormatting: 'auto',
|
||||
insertPragma: false,
|
||||
proseWrap: 'preserve',
|
||||
quoteProps: 'as-needed',
|
||||
requirePragma: false,
|
||||
rangeStart: 0,
|
||||
endOfLine: 'auto',
|
||||
jsxBracketSameLine: false,
|
||||
jsxSingleQuote: false,
|
||||
};
|
||||
96
CHANGELOG.md
96
CHANGELOG.md
@@ -1,5 +1,93 @@
|
||||
# # Changelog
|
||||
<details open>
|
||||
<summary><strong>2023-05-14</strong></summary>
|
||||
|
||||
**Released [v0.4.4](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.4):**
|
||||
|
||||
1. The Msg Clipboard was changed to a checkmark for improved user experience by @techwithanirudh in PR [#247](https://github.com/danny-avila/chatgpt-clone/pull/247).
|
||||
2. A typo in the auth.json path for accessing Google Palm was corrected by @antonme in PR [#266](https://github.com/danny-avila/chatgpt-clone/pull/266).
|
||||
3. @techwithanirudh added a Popup Menu to save sidebar space in PR [#260](https://github.com/danny-avila/chatgpt-clone/pull/260).
|
||||
4. The default pageSize in Conversation.js was increased from 12 to 14 by @danny-avila in PR [#267](https://github.com/danny-avila/chatgpt-clone/pull/267).
|
||||
5. Fonts were updated by @techwithanirudh in PR [#261](https://github.com/danny-avila/chatgpt-clone/pull/261).
|
||||
6. Font file paths in style.css were changed by @danny-avila in PR [#268](https://github.com/danny-avila/chatgpt-clone/pull/268).
|
||||
7. Code was fixed to adjust max_tokens according to model selection by @p4w4n in PR [#263](https://github.com/danny-avila/chatgpt-clone/pull/263).
|
||||
8. Various improvements were made, such as fixing react errors and adjusting the mobile view, by @danny-avila in PR [#269](https://github.com/danny-avila/chatgpt-clone/pull/269).
|
||||
|
||||
New contributors to the project include:
|
||||
|
||||
- @techwithanirudh, who made their first contribution in PR [#247](https://github.com/danny-avila/chatgpt-clone/pull/247).
|
||||
- @antonme, who made their first contribution in PR [#266](https://github.com/danny-avila/chatgpt-clone/pull/266).
|
||||
- @p4w4n, who made their first contribution in PR [#263](https://github.com/danny-avila/chatgpt-clone/pull/263).
|
||||
|
||||
The [full changelog can be found here](https://github.com/danny-avila/chatgpt-clone/compare/v0.4.3...v0.4.4)
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-05-13</strong></summary>
|
||||
|
||||
**Released [v0.4.3](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.3) which now supports Google's PaLM 2!**
|
||||
|
||||

|
||||
|
||||
**How to Setup PaLM 2 (via Google Cloud Vertex AI API)**
|
||||
|
||||
- Enable the Vertex AI API on Google Cloud:
|
||||
- - https://console.cloud.google.com/vertex-ai
|
||||
- Create a Service Account:
|
||||
- - https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create?walkthrough_id=iam--create-service-account#step_index=1
|
||||
- Make sure to click 'Create and Continue' to give at least the 'Vertex AI User' role.
|
||||
- Create a JSON key, rename as 'auth.json' and save it in /api/data/.
|
||||
|
||||
**Alternatively**
|
||||
|
||||
- In your ./api/.env file, set PALM_KEY as "user_provided" to allow the user to provide a Service Account key JSON from the UI.
|
||||
- They will follow the steps above except for renaming the file, simply importing the JSON when prompted.
|
||||
- The key is sent to the server but never saved except in your local storage
|
||||
|
||||
**Note:**
|
||||
|
||||
- Vertex AI does not (yet) support response streaming for text generations, so response may seem to take long when generating a lot of text.
|
||||
- Text streaming is simulated
|
||||
|
||||
|
||||
You can check the full changelog in between v0.4.2 and v0.4.3 [here](https://github.com/danny-avila/chatgpt-clone/compare/v0.4.2...v0.4.3).
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>2023-05-11</strong></summary>
|
||||
|
||||
**Released [v0.4.2](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.2)**
|
||||
|
||||
ChatGPT-Clone received some important upgrades and improvements. A new contributor, [@qcgm1978](https://github.com/qcgm1978), makes their first contribution by adding a null check for adaptiveCards variable. Additionally, support for titling conversations with the Azure endpoint is added by [@danny-avila](https://github.com/danny-avila) in PR [#234](https://github.com/danny-avila/chatgpt-clone/pull/234). In PR [#235](https://github.com/danny-avila/chatgpt-clone/pull/235), [@danny-avila](https://github.com/danny-avila) also makes some necessary fixes to titling, quotation marks, and endpoints being unavailable with only the Azure key provided. The logging system is now powered by Pino and sanitization, thanks to [@danorlando](https://github.com/danorlando) in PR [#227](https://github.com/danny-avila/chatgpt-clone/pull/227). To bulletproof the Docker container, the .dockerignore file is updated to include the client/.env file by [@danny-avila](https://github.com/danny-avila) in PR [#241](https://github.com/danny-avila/chatgpt-clone/pull/241). This issue was brought to our attention on discord.
|
||||
|
||||
There is active work on the new Plugins feature, converting the frontend to Typescript, and looking to integrate Palm2, google's new generative AI accessible via API, to the project as a new endpoint.
|
||||
|
||||
You can check the full changelog in between [v0.4.1](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.1) and [v0.4.2](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.2) [here](https://github.com/danny-avila/chatgpt-clone/compare/v0.4.1...v0.4.2)."
|
||||
|
||||
For discussion and suggestion you can join us: **[community discord server](https://discord.gg/NGaa9RPCft)**
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>2023-05-09</strong></summary>
|
||||
|
||||
**Released [v0.4.1](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.1)**
|
||||
|
||||
* update user system section of readme by @danorlando in #207
|
||||
* remove github-passport and update package.lock files by @danorlando in #208
|
||||
* Update README.md by @fuegovic in #209
|
||||
* fix: fix browser refresh redirecting to /chat/new by @danorlando in #210
|
||||
* fix: fix issue with validation when google account has multiple spaces in username by @danorlando in #211
|
||||
* chore: update docker image version to use latest by @danny-avila in #218
|
||||
* update documentation structure by @fuegovic in #220
|
||||
* Feat: Add Azure support by @danny-avila in #219
|
||||
* Update Message.js by @DavidDev1334 in #191
|
||||
|
||||
⚠️ **IMPORTANT :** Since V0.4.0 You should register and login with a local account (email and password) for the first time sign-up. if you use login for the first time with a social login account (eg. Google, facebook, etc.), the conversations and presets that you created before the user system was implemented will NOT be migrated to that account.
|
||||
|
||||
⚠️ **Breaking - new Env Variables :** Since V0.4.0 You will need to add the new env variables from .env.example for the app to work, even if you're not using multiple users for your purposes.
|
||||
|
||||
For discussion and suggestion you can join us: **[community discord server](https://discord.gg/NGaa9RPCft)**
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>2023-05-07</strong></summary>
|
||||
|
||||
**Released [v0.4.0](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.0)**, Introducing User/Auth System and OAuth2/Social Login! You can now register and login with an email account or use Google login. Your your previous conversations and presets will migrate to your new profile upon creation. Check out the details in the [User/Auth System](#userauth-system) section of the README.md.
|
||||
@@ -10,11 +98,7 @@
|
||||
|
||||
For discussion and suggestion you can join us: **[community discord server](https://discord.gg/NGaa9RPCft)**
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary><strong>Previous Updates</strong></summary>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary><strong>2023-04-05</strong></summary>
|
||||
@@ -45,7 +129,7 @@ The above features are next and then I will have to focus on building the **test
|
||||
|
||||
On that note, I had to switch the default branch due to some breaking changes that haven't been straight forward to debug, mainly related to node-chat-gpt the main dependency of the project. Thankfully, my working branch, now switched to default as main, is working as expected.
|
||||
</details>
|
||||
</details>
|
||||
|
||||
|
||||
##
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Contributors List
|
||||
|
||||
Here is a list of all contributors to this project:
|
||||
We appreciate all the contributors who helped make this project possible:
|
||||
|
||||
- danny-avila (Admin)
|
||||
- wtlyu (Contributor)
|
||||
@@ -8,11 +8,12 @@ Here is a list of all contributors to this project:
|
||||
- alfredo-f (Contributor)
|
||||
- HyunggyuJang (Contributor)
|
||||
- fuegovic (Contributor)
|
||||
- DavidDev1334
|
||||
- toordog (Contributor)
|
||||
- heathriel (External Contributor)
|
||||
- hackreactor-bot (Contributor)
|
||||
- git-bruh (Contributor)
|
||||
zhangsean (Contributor)
|
||||
- zhangsean (Contributor)
|
||||
- llk89 (Contributor)
|
||||
- adamrb (Contributor)
|
||||
|
||||
|
||||
31
Dockerfile
31
Dockerfile
@@ -1,37 +1,32 @@
|
||||
FROM node:19-alpine AS react-client
|
||||
# Base node image
|
||||
FROM node:19-alpine AS base
|
||||
WORKDIR /api
|
||||
COPY /api/package*.json /api/
|
||||
WORKDIR /client
|
||||
# copy package.json into the container at /client
|
||||
COPY /client/package*.json /client/
|
||||
# install dependencies
|
||||
WORKDIR /
|
||||
COPY /package*.json /
|
||||
RUN npm ci
|
||||
# Copy the current directory contents into the container at /client
|
||||
|
||||
# React client build
|
||||
FROM base AS react-client
|
||||
WORKDIR /client
|
||||
COPY /client/ /client/
|
||||
# Set the memory limit for Node.js
|
||||
ENV NODE_OPTIONS="--max-old-space-size=2048"
|
||||
# Build artifacts
|
||||
RUN npm run build
|
||||
|
||||
FROM node:19-alpine AS node-api
|
||||
# Node API setup
|
||||
FROM base AS node-api
|
||||
WORKDIR /api
|
||||
# copy package.json into the container at /api
|
||||
COPY /api/package*.json /api/
|
||||
# install dependencies
|
||||
RUN npm ci
|
||||
# Copy the current directory contents into the container at /api
|
||||
COPY /api/ /api/
|
||||
# Copy the client side code
|
||||
COPY --from=react-client /client/dist /client/dist
|
||||
# Make port 3080 available to the world outside this container
|
||||
EXPOSE 3080
|
||||
# Expose the server to 0.0.0.0
|
||||
ENV HOST=0.0.0.0
|
||||
# Run the app when the container launches
|
||||
CMD ["npm", "start"]
|
||||
|
||||
# Optional: for client with nginx routing
|
||||
FROM nginx:stable-alpine AS nginx-client
|
||||
WORKDIR /usr/share/nginx/html
|
||||
COPY --from=react-client /client/dist /usr/share/nginx/html
|
||||
# Add your nginx.conf
|
||||
COPY /client/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY client/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
ENTRYPOINT ["nginx", "-g", "daemon off;"]
|
||||
|
||||
@@ -6,10 +6,10 @@ WORKDIR /app
|
||||
# Copy package.json files for client and api
|
||||
COPY /client/package*.json /app/client/
|
||||
COPY /api/package*.json /app/api/
|
||||
COPY /package*.json /app/
|
||||
|
||||
# Install dependencies for both client and api
|
||||
RUN cd /app/client && npm ci
|
||||
RUN cd /app/api && npm ci
|
||||
RUN npm ci
|
||||
|
||||
# Copy the current directory contents into the container
|
||||
COPY /client/ /app/client/
|
||||
|
||||
52
README.md
52
README.md
@@ -40,19 +40,34 @@
|
||||
|
||||
##
|
||||
|
||||
<details open>
|
||||
<summary><strong>2023-05-07</strong></summary>
|
||||
|
||||
**Released [v0.4.0](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.0)**, Introducing User/Auth System and OAuth2/Social Login! You can now register and login with an email account or use Google login. Your your previous conversations and presets will migrate to your new profile upon creation. Check out the details in the [User/Auth System](documents/features/user_auth_system.md) section of the README.md.
|
||||
## **Google's PaLM 2 is now supported as of [v0.4.3](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.3)**
|
||||
|
||||

|
||||
|
||||
⚠️ **IMPORTANT :** You should register and login with a local account (email and password) for the first time sign-up. if you use login for the first time with a social login account (eg. Google, facebook, etc.), the conversations and presets that you created before the user system was implemented will NOT be migrated to that account.
|
||||
<details>
|
||||
<summary><strong>How to Setup PaLM 2 (via Google Cloud Vertex AI API)</strong></summary>
|
||||
- Enable the Vertex AI API on Google Cloud:
|
||||
- - https://console.cloud.google.com/vertex-ai
|
||||
- Create a Service Account:
|
||||
- - https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create?walkthrough_id=iam--create-service-account#step_index=1
|
||||
- Make sure to click 'Create and Continue' to give at least the 'Vertex AI User' role.
|
||||
- Create a JSON key, rename as 'auth.json' and save it in /api/data/.
|
||||
|
||||
⚠️ **Breaking - new Env Variables :** You will need to add the new env variables from .env.example for the app to work, even if you're not using multiple users for your purposes.
|
||||
**Alternatively**
|
||||
|
||||
For discussion and suggestion you can join us: **[community discord server](https://discord.gg/NGaa9RPCft)**
|
||||
- In your ./api/.env file, set PALM_KEY as "user_provided" to allow the user to provide a Service Account key JSON from the UI.
|
||||
- They will follow the steps above except for renaming the file, simply importing the JSON when prompted.
|
||||
- The key is sent to the server but never saved except in your local storage
|
||||
|
||||
**Note:**
|
||||
|
||||
- Vertex AI does not (yet) support response streaming for text generations, so response may seem to take long when generating a lot of text.
|
||||
- Text streaming is simulated
|
||||
</details>
|
||||
|
||||
##
|
||||
---
|
||||
|
||||
## [Read all Latest Updates here](CHANGELOG.md)
|
||||
|
||||
<h1>Table of Contents</h1>
|
||||
|
||||
@@ -69,6 +84,7 @@ For discussion and suggestion you can join us: **[community discord server](http
|
||||
<summary><strong>General Information</strong></summary>
|
||||
|
||||
* [Project Origin](documents/general_info/project_origin.md)
|
||||
* [Multilingual Information](documents/general_info/multilingual_information.md)
|
||||
* [Roadmap](documents/general_info/roadmap.md)
|
||||
* [Tech Stack](documents/general_info/tech_stack.md)
|
||||
* [Changelog](CHANGELOG.md)
|
||||
@@ -94,8 +110,11 @@ For discussion and suggestion you can join us: **[community discord server](http
|
||||
* [Code of Conduct](documents/contributions/code_of_conduct.md)
|
||||
* [Contributor Guidelines](documents/contributions/contributor_guidelines.md)
|
||||
* [Documentation Guidelines](documents/contributions/documentation_guidelines.md)
|
||||
* [Code Standards and Conventions](documents/contributions/coding_conventions.md)
|
||||
* [Testing](documents/contributions/testing.md)
|
||||
* [Pull Request Template](documents/contributions/pull_request_template.md)
|
||||
* [Contributors](CONTRIBUTORS.md)
|
||||
* [Trello Board](https://trello.com/b/17z094kq/chatgpt-clone)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -107,9 +126,15 @@ For discussion and suggestion you can join us: **[community discord server](http
|
||||
</details>
|
||||
|
||||
##
|
||||
### [Alternative Documentation](https://chatgpt-clone.gitbook.io/chatgpt-clone-docs/get-started/docker)
|
||||
|
||||
## Contributing
|
||||
##
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#danny-avila/chatgpt-clone&Date)
|
||||
|
||||
## Contributors
|
||||
Contributions and suggestions bug reports and fixes are welcome!
|
||||
Please read the documentation before you do!
|
||||
|
||||
@@ -117,7 +142,8 @@ For new features, components, or extensions, please open an issue and discuss be
|
||||
|
||||
- Join the [Discord community](https://discord.gg/NGaa9RPCft)
|
||||
|
||||
## License
|
||||
This project is licensed under the [MIT License](LICENSE.md).
|
||||
##
|
||||
|
||||
This project exists in its current state thanks to all the people who contribute
|
||||
---
|
||||
<a href="https://github.com/danny-avila/chatgpt-clone/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=danny-avila/chatgpt-clone" />
|
||||
</a>
|
||||
|
||||
47
SECURITY.md
Normal file
47
SECURITY.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
We take security seriously and appreciate the efforts of security researchers to improve the security of our codebase.
|
||||
If you discover a security vulnerability within our project, please follow these guidelines to report it to us:
|
||||
|
||||
**Note: Only report sensible vulnerability report details via Github Security Advisory System. Every other communication channel are public and should be used only to initiate first contact and to initiate a private communication channel.**
|
||||
|
||||
### Communication channels
|
||||
- **Option 1: GitHub Security Advisory System**: We encourage you to use GitHub's Security Advisory system to report any security vulnerabilities you find. This allows us to receive vulnerability reports directly through GitHub. You can find more information on how to submit a security advisory report in the [GitHub Security Advisories documentation](https://docs.github.com/en/code-security/getting-started-with-security-vulnerability-alerts/about-github-security-advisories).
|
||||
- **Option 2: Github issues**: You can initiate first contact via Github Issues. **Please note that initial contact through Discord should not include any sensitive details.**
|
||||
- **Option 3: Discord Server**: You can join our [Discord community](https://discord.gg/NGaa9RPCft) and initiate first contact in the `#issues` channel. **Please note that initial contact through Discord should not include any sensitive details.**
|
||||
|
||||
_After initial contact, we will use this initial contact to establish a private communication channel for further discussion._
|
||||
|
||||
|
||||
### When submitting a vulnerability report, please provide us with the following information:
|
||||
- A clear description of the vulnerability, including steps to reproduce it
|
||||
- The version(s) of the project affected by the vulnerability
|
||||
- Any additional information that may be useful for understanding and addressing the issue
|
||||
We will make every effort to acknowledge your report within 72 hours and keep you informed of its progress towards resolution.
|
||||
|
||||
|
||||
## Security Updates and Patching
|
||||
We are committed to maintaining the security of our open-source project named ChatGPT-Clone and promptly addressing any identified vulnerabilities. To ensure the security of our project, we follow these practices:
|
||||
- We prioritize security updates for the current major release of our software.
|
||||
- We actively monitor the GitHub Security Advisory system and the `#security-reports` channel on Discord for any vulnerability reports.
|
||||
- We promptly review and validate reported vulnerabilities and take appropriate actions to address them.
|
||||
- We release security patches and updates in a timely manner to mitigate any identified vulnerabilities.
|
||||
|
||||
Please note that as a security-conscious community, we may not always disclose detailed information about security issues until we have determined that doing so would not put our users or the project at risk. We appreciate your understanding and cooperation in these matters.
|
||||
|
||||
## Scope
|
||||
This security policy applies to the following GitHub repository:
|
||||
- Repository: [ChatGPT-Clone](https://github.com/danny-avila/chatgpt-clone)
|
||||
|
||||
## Contact
|
||||
If you have any questions or concerns regarding the security of our project, please join our [Discord community](https://discord.gg/NGaa9RPCft) and report them in the appropriate channel.
|
||||
You can also reach out to us by [opening an issue](https://github.com/danny-avila/chatgpt-clone/issues/new) on GitHub.
|
||||
Please note that the response time may vary depending on the nature and severity of the inquiry.
|
||||
|
||||
## Acknowledgments
|
||||
We would like to express our gratitude to the security researchers and community members who help us improve the security of our project. Your contributions are invaluable, and we sincerely appreciate your efforts.
|
||||
|
||||
## Bug Bounty Program
|
||||
We do not currently have a bug bounty program in place. However, we welcome and appreciate any security-related contributions through pull requests (PRs) that address vulnerabilities in our codebase.
|
||||
We believe in the power of collaboration to improve the security of our project and invite you to join us in making it more robust.
|
||||
@@ -24,7 +24,8 @@ MONGO_URI=mongodb://127.0.0.1:27017/chatgpt-clone
|
||||
##########################
|
||||
|
||||
# Access key from OpenAI platform.
|
||||
# Leave it blank to disable this feature.
|
||||
# Leave it blank to disable this feature.
|
||||
# Set to "user_provided" to allow the user to provide their API key from the UI.
|
||||
OPENAI_KEY=
|
||||
|
||||
# Identify the available models, separated by commas *without spaces*.
|
||||
@@ -82,13 +83,43 @@ CHATGPT_TOKEN="user_provided"
|
||||
|
||||
# Identify the available models, separated by commas. The first will be default.
|
||||
# Leave it blank to use internal settings.
|
||||
CHATGPT_MODELS=text-davinci-002-render-sha,text-davinci-002-render-paid,gpt-4
|
||||
CHATGPT_MODELS=text-davinci-002-render-sha,gpt-4
|
||||
# NOTE: you can add gpt-4-plugins, gpt-4-code-interpreter, and gpt-4-browsing to the list above and use the models for these features;
|
||||
# however, the view/display portion of these features are not supported, but you can use the underlying models, which have higher token context
|
||||
# Also: text-davinci-002-render-paid is deprecated as of May 2023
|
||||
|
||||
# Reverse proxy settings for ChatGPT
|
||||
# https://github.com/waylaidwanderer/node-chatgpt-api#using-a-reverse-proxy
|
||||
# By default, the server will use the node-chatgpt-api recommended proxy (a third party server).
|
||||
# CHATGPT_REVERSE_PROXY=
|
||||
|
||||
##########################
|
||||
# PaLM (Google) Endpoint:
|
||||
##########################
|
||||
|
||||
# PaLM 2 Client (via Google Cloud Vertex AI API)
|
||||
# Steps:
|
||||
# Enable the Vertex AI API on Google Cloud:
|
||||
# https://console.cloud.google.com/vertex-ai
|
||||
# Create a Service Account:
|
||||
# https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create?walkthrough_id=iam--create-service-account#step_index=1
|
||||
# Make sure to click 'Create and Continue' to give at least the 'Vertex AI User' role.
|
||||
# Create a JSON key, rename as 'auth.json' and save it in /api/data/.
|
||||
# Alternatively
|
||||
# Uncomment below PALM_KEY and set as "user_provided" to allow the user to provide a Service Account key JSON from the UI.
|
||||
# They will follow the steps above except for renaming the file.
|
||||
# Leave blank or omit to disable this endpoint
|
||||
|
||||
# PALM_KEY="user_provided"
|
||||
|
||||
# In case you need a reverse proxy for this endpoint:
|
||||
# GOOGLE_REVERSE_PROXY=
|
||||
|
||||
##########################
|
||||
# Proxy: To be Used by all endpoints
|
||||
##########################
|
||||
PROXY=
|
||||
|
||||
##########################
|
||||
# Search:
|
||||
##########################
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
es2021: true,
|
||||
node: true
|
||||
},
|
||||
extends: ['eslint:recommended'],
|
||||
overrides: [],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
},
|
||||
rules: {
|
||||
indent: ['error', 2, { SwitchCase: 1 }],
|
||||
'max-len': [
|
||||
'error',
|
||||
{
|
||||
code: 150,
|
||||
ignoreStrings: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
ignoreComments: true
|
||||
}
|
||||
],
|
||||
'linebreak-style': 0,
|
||||
'arrow-parens': [2, 'as-needed', { requireForBlockBody: true }],
|
||||
// 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
|
||||
'no-console': 'off',
|
||||
'import/extensions': 'off',
|
||||
'no-use-before-define': [
|
||||
'error',
|
||||
{
|
||||
functions: false
|
||||
}
|
||||
],
|
||||
'no-promise-executor-return': 'off',
|
||||
'no-param-reassign': 'off',
|
||||
'no-continue': 'off',
|
||||
'no-restricted-syntax': 'off'
|
||||
}
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"singleAttributePerLine": true,
|
||||
"bracketSameLine": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"printWidth": 110,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"parser": "babel"
|
||||
}
|
||||
@@ -16,14 +16,15 @@ const askBing = async ({
|
||||
token,
|
||||
onProgress
|
||||
}) => {
|
||||
const { BingAIClient } = await import('og-chatgpt-api');
|
||||
const { BingAIClient } = await import('@waylaidwanderer/chatgpt-api');
|
||||
const store = {
|
||||
store: new KeyvFile({ filename: './data/cache.json' })
|
||||
};
|
||||
|
||||
const bingAIClient = new BingAIClient({
|
||||
// "_U" cookie from bing.com
|
||||
userToken: process.env.BINGAI_TOKEN == 'user_provided' ? token : process.env.BINGAI_TOKEN ?? null,
|
||||
userToken:
|
||||
process.env.BINGAI_TOKEN == 'user_provided' ? token : process.env.BINGAI_TOKEN ?? null,
|
||||
// If the above doesn't work, provide all your cookies as a string instead
|
||||
// cookies: '',
|
||||
debug: false,
|
||||
|
||||
@@ -11,16 +11,18 @@ const browserClient = async ({
|
||||
abortController,
|
||||
userId
|
||||
}) => {
|
||||
const { ChatGPTBrowserClient } = await import('og-chatgpt-api');
|
||||
const { ChatGPTBrowserClient } = await import('@waylaidwanderer/chatgpt-api');
|
||||
const store = {
|
||||
store: new KeyvFile({ filename: './data/cache.json' })
|
||||
};
|
||||
|
||||
const clientOptions = {
|
||||
// Warning: This will expose your access token to a third party. Consider the risks before using this.
|
||||
reverseProxyUrl: process.env.CHATGPT_REVERSE_PROXY || 'https://ai.fakeopen.com/api/conversation',
|
||||
reverseProxyUrl:
|
||||
process.env.CHATGPT_REVERSE_PROXY || 'https://ai.fakeopen.com/api/conversation',
|
||||
// Access token from https://chat.openai.com/api/auth/session
|
||||
accessToken: process.env.CHATGPT_TOKEN == 'user_provided' ? token : process.env.CHATGPT_TOKEN ?? null,
|
||||
accessToken:
|
||||
process.env.CHATGPT_TOKEN == 'user_provided' ? token : process.env.CHATGPT_TOKEN ?? null,
|
||||
model: model,
|
||||
debug: false,
|
||||
proxy: process.env.PROXY || null,
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
require('dotenv').config();
|
||||
const { KeyvFile } = require('keyv-file');
|
||||
const { genAzureEndpoint } = require('../../utils/genAzureEndpoints');
|
||||
const tiktoken = require('@dqbd/tiktoken');
|
||||
const encoding_for_model = tiktoken.encoding_for_model;
|
||||
|
||||
const askClient = async ({
|
||||
text,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
model,
|
||||
oaiApiKey,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
temperature,
|
||||
@@ -22,12 +26,18 @@ const askClient = async ({
|
||||
};
|
||||
|
||||
const azure = process.env.AZURE_OPENAI_API_KEY ? true : false;
|
||||
|
||||
if (promptPrefix == null) {
|
||||
promptText = 'You are ChatGPT, a large language model trained by OpenAI.';
|
||||
} else {
|
||||
promptText = promptPrefix;
|
||||
}
|
||||
const maxContextTokens = model === 'gpt-4' ? 8191 : model === 'gpt-4-32k' ? 32767 : 4095; // 1 less than maximum
|
||||
const clientOptions = {
|
||||
reverseProxyUrl: process.env.OPENAI_REVERSE_PROXY || null,
|
||||
azure,
|
||||
maxContextTokens,
|
||||
modelOptions: {
|
||||
model: model,
|
||||
model,
|
||||
temperature,
|
||||
top_p,
|
||||
presence_penalty,
|
||||
@@ -35,27 +45,48 @@ const askClient = async ({
|
||||
},
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
proxy: process.env.PROXY || null,
|
||||
debug: false
|
||||
proxy: process.env.PROXY || null
|
||||
// debug: true
|
||||
};
|
||||
|
||||
let apiKey = process.env.OPENAI_KEY;
|
||||
let apiKey = oaiApiKey ? oaiApiKey : process.env.OPENAI_KEY || null;
|
||||
|
||||
if (azure) {
|
||||
apiKey = process.env.AZURE_OPENAI_API_KEY;
|
||||
clientOptions.reverseProxyUrl = `https://${process.env.AZURE_OPENAI_API_INSTANCE_NAME}.openai.azure.com/openai/deployments/${process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME}/chat/completions?api-version=${process.env.AZURE_OPENAI_API_VERSION}`;
|
||||
apiKey = oaiApiKey ? oaiApiKey : process.env.AZURE_OPENAI_API_KEY || null;
|
||||
clientOptions.reverseProxyUrl = genAzureEndpoint({
|
||||
azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME,
|
||||
azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME,
|
||||
azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION
|
||||
});
|
||||
}
|
||||
|
||||
const client = new ChatGPTClient(apiKey, clientOptions, store);
|
||||
|
||||
|
||||
const options = {
|
||||
onProgress,
|
||||
abortController,
|
||||
...(parentMessageId && conversationId ? { parentMessageId, conversationId } : {})
|
||||
};
|
||||
|
||||
const enc = encoding_for_model(model);
|
||||
const text_tokens = enc.encode(text);
|
||||
const prompt_tokens = enc.encode(promptText);
|
||||
// console.log("Prompt tokens = ", prompt_tokens.length);
|
||||
// console.log("Message Tokens = ", text_tokens.length);
|
||||
|
||||
const res = await client.sendMessage(text, { ...options, userId });
|
||||
return res;
|
||||
// return res;
|
||||
// create a new response object that includes the token counts
|
||||
const newRes = {
|
||||
...res,
|
||||
usage: {
|
||||
prompt_tokens: prompt_tokens.length,
|
||||
completion_tokens: text_tokens.length,
|
||||
total_tokens: prompt_tokens.length + text_tokens.length
|
||||
}
|
||||
};
|
||||
|
||||
return newRes;
|
||||
};
|
||||
|
||||
module.exports = { askClient };
|
||||
|
||||
397
api/app/google/GoogleClient.js
Normal file
397
api/app/google/GoogleClient.js
Normal file
@@ -0,0 +1,397 @@
|
||||
const crypto = require('crypto');
|
||||
const TextStream = require('../stream');
|
||||
const { google } = require('googleapis');
|
||||
const { Agent, ProxyAgent } = require('undici');
|
||||
const { getMessages, saveMessage, saveConvo } = require('../../models');
|
||||
const {
|
||||
encoding_for_model: encodingForModel,
|
||||
get_encoding: getEncoding
|
||||
} = require('@dqbd/tiktoken');
|
||||
|
||||
const tokenizersCache = {};
|
||||
|
||||
class GoogleAgent {
|
||||
constructor(credentials, options = {}) {
|
||||
this.client_email = credentials.client_email;
|
||||
this.project_id = credentials.project_id;
|
||||
this.private_key = credentials.private_key;
|
||||
this.setOptions(options);
|
||||
this.currentDateString = new Date().toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
constructUrl() {
|
||||
return `https://us-central1-aiplatform.googleapis.com/v1/projects/${this.project_id}/locations/us-central1/publishers/google/models/${this.modelOptions.model}:predict`;
|
||||
}
|
||||
|
||||
setOptions(options) {
|
||||
if (this.options && !this.options.replaceOptions) {
|
||||
// nested options aren't spread properly, so we need to do this manually
|
||||
this.options.modelOptions = {
|
||||
...this.options.modelOptions,
|
||||
...options.modelOptions
|
||||
};
|
||||
delete options.modelOptions;
|
||||
// now we can merge options
|
||||
this.options = {
|
||||
...this.options,
|
||||
...options
|
||||
};
|
||||
} else {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
this.options.examples = this.options.examples.filter(
|
||||
(obj) => obj.input.content !== '' && obj.output.content !== ''
|
||||
);
|
||||
|
||||
const modelOptions = this.options.modelOptions || {};
|
||||
this.modelOptions = {
|
||||
...modelOptions,
|
||||
// set some good defaults (check for undefined in some cases because they may be 0)
|
||||
model: modelOptions.model || 'chat-bison',
|
||||
temperature: typeof modelOptions.temperature === 'undefined' ? 0.2 : modelOptions.temperature, // 0 - 1, 0.2 is recommended
|
||||
topP: typeof modelOptions.topP === 'undefined' ? 0.95 : modelOptions.topP, // 0 - 1, default: 0.95
|
||||
topK: typeof modelOptions.topK === 'undefined' ? 40 : modelOptions.topK // 1-40, default: 40
|
||||
// stop: modelOptions.stop // no stop method for now
|
||||
};
|
||||
|
||||
this.isChatModel = this.modelOptions.model.startsWith('chat-');
|
||||
const { isChatModel } = this;
|
||||
this.isTextModel = this.modelOptions.model.startsWith('text-');
|
||||
const { isTextModel } = this;
|
||||
|
||||
this.maxContextTokens = this.options.maxContextTokens || (isTextModel ? 8000 : 4096);
|
||||
// The max prompt tokens is determined by the max context tokens minus the max response tokens.
|
||||
// Earlier messages will be dropped until the prompt is within the limit.
|
||||
this.maxResponseTokens = this.modelOptions.maxOutputTokens || 1024;
|
||||
this.maxPromptTokens =
|
||||
this.options.maxPromptTokens || this.maxContextTokens - this.maxResponseTokens;
|
||||
|
||||
if (this.maxPromptTokens + this.maxResponseTokens > this.maxContextTokens) {
|
||||
throw new Error(
|
||||
`maxPromptTokens + maxOutputTokens (${this.maxPromptTokens} + ${this.maxResponseTokens} = ${
|
||||
this.maxPromptTokens + this.maxResponseTokens
|
||||
}) must be less than or equal to maxContextTokens (${this.maxContextTokens})`
|
||||
);
|
||||
}
|
||||
|
||||
this.userLabel = this.options.userLabel || 'User';
|
||||
this.modelLabel = this.options.modelLabel || 'Assistant';
|
||||
|
||||
if (isChatModel) {
|
||||
// Use these faux tokens to help the AI understand the context since we are building the chat log ourselves.
|
||||
// Trying to use "<|im_start|>" causes the AI to still generate "<" or "<|" at the end sometimes for some reason,
|
||||
// without tripping the stop sequences, so I'm using "||>" instead.
|
||||
this.startToken = '||>';
|
||||
this.endToken = '';
|
||||
this.gptEncoder = this.constructor.getTokenizer('cl100k_base');
|
||||
} else if (isTextModel) {
|
||||
this.startToken = '<|im_start|>';
|
||||
this.endToken = '<|im_end|>';
|
||||
this.gptEncoder = this.constructor.getTokenizer('text-davinci-003', true, {
|
||||
'<|im_start|>': 100264,
|
||||
'<|im_end|>': 100265
|
||||
});
|
||||
} else {
|
||||
// Previously I was trying to use "<|endoftext|>" but there seems to be some bug with OpenAI's token counting
|
||||
// system that causes only the first "<|endoftext|>" to be counted as 1 token, and the rest are not treated
|
||||
// as a single token. So we're using this instead.
|
||||
this.startToken = '||>';
|
||||
this.endToken = '';
|
||||
try {
|
||||
this.gptEncoder = this.constructor.getTokenizer(this.modelOptions.model, true);
|
||||
} catch {
|
||||
this.gptEncoder = this.constructor.getTokenizer('text-davinci-003', true);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.modelOptions.stop) {
|
||||
const stopTokens = [this.startToken];
|
||||
if (this.endToken && this.endToken !== this.startToken) {
|
||||
stopTokens.push(this.endToken);
|
||||
}
|
||||
stopTokens.push(`\n${this.userLabel}:`);
|
||||
stopTokens.push('<|diff_marker|>');
|
||||
// I chose not to do one for `modelLabel` because I've never seen it happen
|
||||
this.modelOptions.stop = stopTokens;
|
||||
}
|
||||
|
||||
if (this.options.reverseProxyUrl) {
|
||||
this.completionsUrl = this.options.reverseProxyUrl;
|
||||
} else {
|
||||
this.completionsUrl = this.constructUrl();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
static getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) {
|
||||
if (tokenizersCache[encoding]) {
|
||||
return tokenizersCache[encoding];
|
||||
}
|
||||
let tokenizer;
|
||||
if (isModelName) {
|
||||
tokenizer = encodingForModel(encoding, extendSpecialTokens);
|
||||
} else {
|
||||
tokenizer = getEncoding(encoding, extendSpecialTokens);
|
||||
}
|
||||
tokenizersCache[encoding] = tokenizer;
|
||||
return tokenizer;
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
const scopes = ['https://www.googleapis.com/auth/cloud-platform'];
|
||||
const jwtClient = new google.auth.JWT(this.client_email, null, this.private_key, scopes);
|
||||
|
||||
jwtClient.authorize((err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
return jwtClient;
|
||||
}
|
||||
|
||||
buildPayload(input, { messages = [] }) {
|
||||
let payload = {
|
||||
instances: [
|
||||
{
|
||||
messages: [...messages, { author: this.userLabel, content: input }]
|
||||
}
|
||||
],
|
||||
parameters: this.options.modelOptions
|
||||
};
|
||||
|
||||
if (this.options.promptPrefix) {
|
||||
payload.instances[0].context = this.options.promptPrefix;
|
||||
}
|
||||
|
||||
if (this.options.examples.length > 0) {
|
||||
payload.instances[0].examples = this.options.examples;
|
||||
}
|
||||
|
||||
if (this.isTextModel) {
|
||||
payload.instances = [
|
||||
{
|
||||
prompt: input
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('buildPayload');
|
||||
console.dir(payload, { depth: null });
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
async getCompletion(input, messages = [], abortController = null) {
|
||||
if (!abortController) {
|
||||
abortController = new AbortController();
|
||||
}
|
||||
const { debug } = this.options;
|
||||
const url = this.completionsUrl;
|
||||
if (debug) {
|
||||
console.debug();
|
||||
console.debug(url);
|
||||
console.debug(this.modelOptions);
|
||||
console.debug();
|
||||
}
|
||||
const opts = {
|
||||
method: 'POST',
|
||||
agent: new Agent({
|
||||
bodyTimeout: 0,
|
||||
headersTimeout: 0
|
||||
}),
|
||||
signal: abortController.signal
|
||||
};
|
||||
|
||||
if (this.options.proxy) {
|
||||
opts.agent = new ProxyAgent(this.options.proxy);
|
||||
}
|
||||
|
||||
const client = await this.getClient();
|
||||
const payload = this.buildPayload(input, { messages });
|
||||
const res = await client.request({ url, method: 'POST', data: payload });
|
||||
console.dir(res.data, { depth: null });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async loadHistory(conversationId, parentMessageId = null) {
|
||||
if (this.options.debug) {
|
||||
console.debug('Loading history for conversation', conversationId, parentMessageId);
|
||||
}
|
||||
|
||||
if (!parentMessageId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const messages = (await getMessages({ conversationId })) || [];
|
||||
|
||||
if (messages.length === 0) {
|
||||
this.currentMessages = [];
|
||||
return [];
|
||||
}
|
||||
|
||||
const orderedMessages = this.constructor.getMessagesForConversation(messages, parentMessageId);
|
||||
return orderedMessages.map((message) => {
|
||||
return {
|
||||
author: message.isCreatedByUser ? this.userLabel : this.modelLabel,
|
||||
content: message.content
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async saveMessageToDatabase(message, user = null) {
|
||||
await saveMessage({ ...message, unfinished: false });
|
||||
await saveConvo(user, {
|
||||
conversationId: message.conversationId,
|
||||
endpoint: 'google',
|
||||
...this.modelOptions
|
||||
});
|
||||
}
|
||||
|
||||
async sendMessage(message, opts = {}) {
|
||||
if (opts && typeof opts === 'object') {
|
||||
this.setOptions(opts);
|
||||
}
|
||||
console.log('sendMessage', message, opts);
|
||||
|
||||
const user = opts.user || null;
|
||||
const conversationId = opts.conversationId || crypto.randomUUID();
|
||||
const parentMessageId = opts.parentMessageId || '00000000-0000-0000-0000-000000000000';
|
||||
const userMessageId = crypto.randomUUID();
|
||||
const responseMessageId = crypto.randomUUID();
|
||||
const messages = await this.loadHistory(conversationId, this.options?.parentMessageId);
|
||||
|
||||
const userMessage = {
|
||||
messageId: userMessageId,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
sender: 'User',
|
||||
text: message,
|
||||
isCreatedByUser: true
|
||||
};
|
||||
|
||||
if (typeof opts?.getIds === 'function') {
|
||||
opts.getIds({
|
||||
userMessage,
|
||||
conversationId,
|
||||
responseMessageId
|
||||
});
|
||||
}
|
||||
|
||||
console.log('userMessage', userMessage);
|
||||
|
||||
await this.saveMessageToDatabase(userMessage, user);
|
||||
let reply = '';
|
||||
let blocked = false;
|
||||
try {
|
||||
const result = await this.getCompletion(message, messages, opts.abortController);
|
||||
blocked = result?.predictions?.[0]?.safetyAttributes?.blocked;
|
||||
reply =
|
||||
result?.predictions?.[0]?.candidates?.[0]?.content ||
|
||||
result?.predictions?.[0]?.content ||
|
||||
'';
|
||||
if (blocked === true) {
|
||||
reply = `Google blocked a proper response to your message:\n${JSON.stringify(
|
||||
result.predictions[0].safetyAttributes
|
||||
)}${reply.length > 0 ? `\nAI Response:\n${reply}` : ''}`;
|
||||
}
|
||||
if (this.options.debug) {
|
||||
console.debug('result');
|
||||
console.debug(result);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
if (this.options.debug) {
|
||||
console.debug('options');
|
||||
console.debug(this.options);
|
||||
}
|
||||
|
||||
if (!blocked) {
|
||||
const textStream = new TextStream(reply, { delay: 0.5 });
|
||||
await textStream.processTextStream(opts.onProgress);
|
||||
}
|
||||
|
||||
const responseMessage = {
|
||||
messageId: responseMessageId,
|
||||
conversationId,
|
||||
parentMessageId: userMessage.messageId,
|
||||
sender: 'PaLM2',
|
||||
text: reply,
|
||||
error: blocked,
|
||||
isCreatedByUser: false
|
||||
};
|
||||
|
||||
await this.saveMessageToDatabase(responseMessage, user);
|
||||
return responseMessage;
|
||||
}
|
||||
|
||||
getTokenCount(text) {
|
||||
return this.gptEncoder.encode(text, 'all').length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm adapted from "6. Counting tokens for chat API calls" of
|
||||
* https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
|
||||
*
|
||||
* An additional 2 tokens need to be added for metadata after all messages have been counted.
|
||||
*
|
||||
* @param {*} message
|
||||
*/
|
||||
getTokenCountForMessage(message) {
|
||||
// Map each property of the message to the number of tokens it contains
|
||||
const propertyTokenCounts = Object.entries(message).map(([key, value]) => {
|
||||
// Count the number of tokens in the property value
|
||||
const numTokens = this.getTokenCount(value);
|
||||
|
||||
// Subtract 1 token if the property key is 'name'
|
||||
const adjustment = key === 'name' ? 1 : 0;
|
||||
return numTokens - adjustment;
|
||||
});
|
||||
|
||||
// Sum the number of tokens in all properties and add 4 for metadata
|
||||
return propertyTokenCounts.reduce((a, b) => a + b, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through messages, building an array based on the parentMessageId.
|
||||
* Each message has an id and a parentMessageId. The parentMessageId is the id of the message that this message is a reply to.
|
||||
* @param messages
|
||||
* @param parentMessageId
|
||||
* @returns {*[]} An array containing the messages in the order they should be displayed, starting with the root message.
|
||||
*/
|
||||
static getMessagesForConversation(messages, parentMessageId) {
|
||||
const orderedMessages = [];
|
||||
let currentMessageId = parentMessageId;
|
||||
while (currentMessageId) {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
const message = messages.find((m) => m.messageId === currentMessageId);
|
||||
if (!message) {
|
||||
break;
|
||||
}
|
||||
orderedMessages.unshift(message);
|
||||
currentMessageId = message.parentMessageId;
|
||||
}
|
||||
|
||||
if (orderedMessages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return orderedMessages.map((msg) => ({
|
||||
isCreatedByUser: msg.isCreatedByUser,
|
||||
content: msg.text
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GoogleAgent;
|
||||
59
api/app/stream.js
Normal file
59
api/app/stream.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const { Readable } = require('stream');
|
||||
|
||||
class TextStream extends Readable {
|
||||
constructor(text, options = {}) {
|
||||
super(options);
|
||||
this.text = text;
|
||||
this.currentIndex = 0;
|
||||
this.delay = options.delay || 20; // Time in milliseconds
|
||||
}
|
||||
|
||||
_read() {
|
||||
const minChunkSize = 2;
|
||||
const maxChunkSize = 4;
|
||||
const { delay } = this;
|
||||
|
||||
if (this.currentIndex < this.text.length) {
|
||||
setTimeout(() => {
|
||||
const remainingChars = this.text.length - this.currentIndex;
|
||||
const chunkSize = Math.min(this.randomInt(minChunkSize, maxChunkSize + 1), remainingChars);
|
||||
|
||||
const chunk = this.text.slice(this.currentIndex, this.currentIndex + chunkSize);
|
||||
this.push(chunk);
|
||||
this.currentIndex += chunkSize;
|
||||
}, delay);
|
||||
} else {
|
||||
this.push(null); // signal end of data
|
||||
}
|
||||
}
|
||||
|
||||
randomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min)) + min;
|
||||
}
|
||||
|
||||
async processTextStream(onProgressCallback) {
|
||||
const streamPromise = new Promise((resolve, reject) => {
|
||||
this.on('data', (chunk) => {
|
||||
onProgressCallback(chunk.toString());
|
||||
});
|
||||
|
||||
this.on('end', () => {
|
||||
console.log('Stream ended');
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await streamPromise;
|
||||
} catch (err) {
|
||||
console.error('Error processing text stream:', err);
|
||||
// Handle the error appropriately, e.g., return an error message or throw an error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TextStream;
|
||||
@@ -1,7 +1,8 @@
|
||||
const { Configuration, OpenAIApi } = require('openai');
|
||||
const _ = require('lodash');
|
||||
const { genAzureEndpoint } = require('../utils/genAzureEndpoints');
|
||||
|
||||
const proxyEnvToAxiosProxy = proxyString => {
|
||||
const proxyEnvToAxiosProxy = (proxyString) => {
|
||||
if (!proxyString) return null;
|
||||
|
||||
const regex = /^([^:]+):\/\/(?:([^:@]*):?([^:@]*)@)?([^:]+)(?::(\d+))?/;
|
||||
@@ -16,7 +17,7 @@ const proxyEnvToAxiosProxy = proxyString => {
|
||||
return proxyConfig;
|
||||
};
|
||||
|
||||
const titleConvo = async ({ endpoint, text, response }) => {
|
||||
const titleConvo = async ({ endpoint, text, response, oaiApiKey }) => {
|
||||
let title = 'New Chat';
|
||||
const ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default;
|
||||
|
||||
@@ -33,7 +34,9 @@ const titleConvo = async ({ endpoint, text, response }) => {
|
||||
||>Title:`
|
||||
};
|
||||
|
||||
const azure = process.env.AZURE_OPENAI_API_KEY ? true : false;
|
||||
const options = {
|
||||
azure,
|
||||
reverseProxyUrl: process.env.OPENAI_REVERSE_PROXY || null,
|
||||
proxy: process.env.PROXY || null
|
||||
};
|
||||
@@ -47,9 +50,20 @@ const titleConvo = async ({ endpoint, text, response }) => {
|
||||
frequency_penalty: 0
|
||||
};
|
||||
|
||||
const titleGenClient = new ChatGPTClient(process.env.OPENAI_KEY, titleGenClientOptions);
|
||||
let apiKey = oaiApiKey || process.env.OPENAI_KEY;
|
||||
|
||||
if (azure) {
|
||||
apiKey = process.env.AZURE_OPENAI_API_KEY;
|
||||
titleGenClientOptions.reverseProxyUrl = genAzureEndpoint({
|
||||
azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME,
|
||||
azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME,
|
||||
azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION
|
||||
});
|
||||
}
|
||||
|
||||
const titleGenClient = new ChatGPTClient(apiKey, titleGenClientOptions);
|
||||
const result = await titleGenClient.getCompletion([instructionsPayload], null);
|
||||
title = result.choices[0].message.content.replace(/\s+/g, ' ').trim();
|
||||
title = result.choices[0].message.content.replace(/\s+/g, ' ').replaceAll('"', '').trim();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.log('There was an issue generating title, see error above');
|
||||
|
||||
@@ -110,7 +110,7 @@ async function migrateDb() {
|
||||
ret[0] = await migrateToStrictFollowParentMessageIdChain();
|
||||
ret[1] = await migrateToSupportBetterCustomization();
|
||||
|
||||
const isMigrated = !!ret.find(element => !element?.noNeed);
|
||||
const isMigrated = !!ret.find((element) => !element?.noNeed);
|
||||
|
||||
if (!isMigrated) console.log('[Migrate] Nothing to migrate');
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
const regex = / \[.*?]\(.*?\)/g;
|
||||
|
||||
const getCitations = (res) => {
|
||||
const textBlocks = res.details.adaptiveCards[0].body;
|
||||
const adaptiveCards = res.details.adaptiveCards;
|
||||
const textBlocks = adaptiveCards && adaptiveCards[0].body;
|
||||
if (!textBlocks) return '';
|
||||
let links = textBlocks[textBlocks.length - 1]?.text.match(regex);
|
||||
if (links?.length === 0 || !links) return '';
|
||||
@@ -10,4 +11,4 @@ const getCitations = (res) => {
|
||||
return links.join('\n');
|
||||
};
|
||||
|
||||
module.exports = getCitations;
|
||||
module.exports = getCitations;
|
||||
|
||||
@@ -2,11 +2,11 @@ function mergeSort(arr, compareFn) {
|
||||
if (arr.length <= 1) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
|
||||
const mid = Math.floor(arr.length / 2);
|
||||
const leftArr = arr.slice(0, mid);
|
||||
const rightArr = arr.slice(mid);
|
||||
|
||||
|
||||
return merge(mergeSort(leftArr, compareFn), mergeSort(rightArr, compareFn), compareFn);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ function merge(leftArr, rightArr, compareFn) {
|
||||
const result = [];
|
||||
let leftIndex = 0;
|
||||
let rightIndex = 0;
|
||||
|
||||
|
||||
while (leftIndex < leftArr.length && rightIndex < rightArr.length) {
|
||||
if (compareFn(leftArr[leftIndex], rightArr[rightIndex]) < 0) {
|
||||
result.push(leftArr[leftIndex++]);
|
||||
@@ -22,8 +22,8 @@ function merge(leftArr, rightArr, compareFn) {
|
||||
result.push(rightArr[rightIndex++]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result.concat(leftArr.slice(leftIndex)).concat(rightArr.slice(rightIndex));
|
||||
}
|
||||
|
||||
module.exports = mergeSort;
|
||||
module.exports = mergeSort;
|
||||
|
||||
@@ -19,7 +19,7 @@ const requireLocalAuth = (req, res, next) => {
|
||||
}
|
||||
if (!user) {
|
||||
log({
|
||||
title: '(requireLocalAuth) Error: No user',
|
||||
title: '(requireLocalAuth) Error: No user'
|
||||
});
|
||||
return res.status(422).send(info);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ module.exports = {
|
||||
return { message: 'Error saving conversation' };
|
||||
}
|
||||
},
|
||||
getConvosByPage: async (user, pageNumber = 1, pageSize = 12) => {
|
||||
getConvosByPage: async (user, pageNumber = 1, pageSize = 14) => {
|
||||
try {
|
||||
const totalConvos = (await Conversation.countDocuments({ user })) || 1;
|
||||
const totalPages = Math.ceil(totalConvos / pageSize);
|
||||
@@ -45,7 +45,7 @@ module.exports = {
|
||||
return { message: 'Error getting conversations' };
|
||||
}
|
||||
},
|
||||
getConvosQueried: async (user, convoIds, pageNumber = 1, pageSize = 12) => {
|
||||
getConvosQueried: async (user, convoIds, pageNumber = 1, pageSize = 14) => {
|
||||
try {
|
||||
if (!convoIds || convoIds.length === 0) {
|
||||
return { conversations: [], pages: 1, pageNumber, pageSize };
|
||||
|
||||
@@ -2,7 +2,7 @@ const Message = require('./schema/messageSchema');
|
||||
|
||||
module.exports = {
|
||||
Message,
|
||||
|
||||
|
||||
async saveMessage({
|
||||
messageId,
|
||||
newMessageId,
|
||||
@@ -32,7 +32,7 @@ module.exports = {
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
|
||||
|
||||
return {
|
||||
messageId,
|
||||
conversationId,
|
||||
@@ -41,13 +41,12 @@ module.exports = {
|
||||
text,
|
||||
isCreatedByUser
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
console.error(`Error saving message: ${err}`);
|
||||
throw new Error('Failed to save message.');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
async deleteMessagesSince({ messageId, conversationId }) {
|
||||
try {
|
||||
const message = await Message.findOne({ messageId }).exec();
|
||||
@@ -57,27 +56,24 @@ module.exports = {
|
||||
.deleteMany({ createdAt: { $gt: message.createdAt } })
|
||||
.exec();
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(`Error deleting messages: ${err}`);
|
||||
throw new Error('Failed to delete messages.');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
async getMessages(filter) {
|
||||
try {
|
||||
return await Message.find(filter).sort({ createdAt: 1 }).exec();
|
||||
|
||||
} catch (err) {
|
||||
console.error(`Error getting messages: ${err}`);
|
||||
throw new Error('Failed to get messages.');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
async deleteMessages(filter) {
|
||||
try {
|
||||
return await Message.deleteMany(filter).exec();
|
||||
|
||||
} catch (err) {
|
||||
console.error(`Error deleting messages: ${err}`);
|
||||
throw new Error('Failed to delete messages.');
|
||||
|
||||
@@ -39,7 +39,7 @@ module.exports = {
|
||||
},
|
||||
deletePresets: async (user, filter) => {
|
||||
let toRemove = await Preset.find({ ...filter, user }).select('presetId');
|
||||
const ids = toRemove.map(instance => instance.presetId);
|
||||
const ids = toRemove.map((instance) => instance.presetId);
|
||||
let deleteCount = await Preset.deleteMany({ ...filter, user }).exec();
|
||||
return deleteCount;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const promptSchema = mongoose.Schema({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
const promptSchema = mongoose.Schema(
|
||||
{
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
prompt: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
category: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
prompt: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
},
|
||||
}, { timestamps: true });
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const Prompt = mongoose.models.Prompt || mongoose.model('Prompt', promptSchema);
|
||||
|
||||
@@ -31,7 +34,7 @@ module.exports = {
|
||||
},
|
||||
getPrompts: async (filter) => {
|
||||
try {
|
||||
return await Prompt.find(filter).exec()
|
||||
return await Prompt.find(filter).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { prompt: 'Error getting prompts' };
|
||||
@@ -39,10 +42,10 @@ module.exports = {
|
||||
},
|
||||
deletePrompts: async (filter) => {
|
||||
try {
|
||||
return await Prompt.deleteMany(filter).exec()
|
||||
return await Prompt.deleteMany(filter).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { prompt: 'Error deleting prompts' };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -79,7 +79,7 @@ const userSchema = mongoose.Schema(
|
||||
|
||||
//Remove refreshToken from the response
|
||||
userSchema.set('toJSON', {
|
||||
transform: function (doc, ret, options) {
|
||||
transform: function (_doc, ret,) {
|
||||
delete ret.refreshToken;
|
||||
return ret;
|
||||
}
|
||||
@@ -142,7 +142,6 @@ userSchema.methods.comparePassword = function (candidatePassword, callback) {
|
||||
};
|
||||
|
||||
module.exports.hashPassword = async (password) => {
|
||||
|
||||
const hashedPassword = await new Promise((resolve, reject) => {
|
||||
bcrypt.hash(password, 10, function (err, hash) {
|
||||
if (err) reject(err);
|
||||
@@ -169,7 +168,7 @@ module.exports.validateUser = (user) => {
|
||||
password: Joi.string().min(8).max(60).allow('').allow(null)
|
||||
};
|
||||
|
||||
return Joi.validate(user, schema);
|
||||
return schema.validate(user);
|
||||
};
|
||||
|
||||
const User = mongoose.model('User', userSchema);
|
||||
|
||||
@@ -19,10 +19,7 @@ const createMeiliMongooseModel = function ({ index, indexName, client, attribute
|
||||
static async clearMeiliIndex() {
|
||||
await index.delete();
|
||||
// await index.deleteAllDocuments();
|
||||
await this.collection.updateMany(
|
||||
{ _meiliIndex: true },
|
||||
{ $set: { _meiliIndex: false } }
|
||||
);
|
||||
await this.collection.updateMany({ _meiliIndex: true }, { $set: { _meiliIndex: false } });
|
||||
}
|
||||
|
||||
static async resetIndex() {
|
||||
@@ -57,7 +54,7 @@ const createMeiliMongooseModel = function ({ index, indexName, client, attribute
|
||||
// Find objects into mongodb matching `objectID` from Meili search
|
||||
const query = {};
|
||||
// query[primaryKey] = { $in: _.map(data.hits, primaryKey) };
|
||||
query[primaryKey] = _.map(data.hits, hit => cleanUpPrimaryKeyValue(hit[primaryKey]));
|
||||
query[primaryKey] = _.map(data.hits, (hit) => cleanUpPrimaryKeyValue(hit[primaryKey]));
|
||||
// console.log('query', query);
|
||||
const hitsFromMongoose = await this.find(
|
||||
query,
|
||||
@@ -67,7 +64,7 @@ const createMeiliMongooseModel = function ({ index, indexName, client, attribute
|
||||
return { ...results, [key]: 1 };
|
||||
},
|
||||
{ _id: 1 }
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Add additional data from mongodb into Meili search hits
|
||||
@@ -198,8 +195,8 @@ module.exports = function mongoMeili(schema, options) {
|
||||
if (Object.prototype.hasOwnProperty.call(schema.obj, 'messages')) {
|
||||
console.log('Syncing convos...');
|
||||
mongoose.model('Conversation').syncWithMeili();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(schema.obj, 'messageId')) {
|
||||
console.log('Syncing messages...');
|
||||
mongoose.model('Message').syncWithMeili();
|
||||
|
||||
@@ -17,6 +17,12 @@ module.exports = {
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
// for google only
|
||||
modelLabel: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
promptPrefix: {
|
||||
type: String,
|
||||
default: null,
|
||||
@@ -32,6 +38,22 @@ module.exports = {
|
||||
default: 1,
|
||||
required: false
|
||||
},
|
||||
// for google only
|
||||
topP: {
|
||||
type: Number,
|
||||
default: 0.95,
|
||||
required: false
|
||||
},
|
||||
topK: {
|
||||
type: Number,
|
||||
default: 40,
|
||||
required: false
|
||||
},
|
||||
maxOutputTokens: {
|
||||
type: Number,
|
||||
default: 1024,
|
||||
required: false
|
||||
},
|
||||
presence_penalty: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
|
||||
@@ -20,6 +20,8 @@ const convoSchema = mongoose.Schema(
|
||||
default: null
|
||||
},
|
||||
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }],
|
||||
// google only
|
||||
examples: [{ type: mongoose.Schema.Types.Mixed }],
|
||||
...conversationPreset,
|
||||
// for bingAI only
|
||||
jailbreakConversationId: {
|
||||
|
||||
@@ -17,6 +17,8 @@ const presetSchema = mongoose.Schema(
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
// google only
|
||||
examples: [{ type: mongoose.Schema.Types.Mixed }],
|
||||
...conversationPreset
|
||||
},
|
||||
{ timestamps: true }
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
const mongoose = require("mongoose");
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const tokenSchema = new Schema({
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
required: true,
|
||||
ref: "user",
|
||||
ref: 'user'
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
required: true,
|
||||
default: Date.now,
|
||||
expires: 900,
|
||||
},
|
||||
expires: 900
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("Token", tokenSchema);
|
||||
module.exports = mongoose.model('Token', tokenSchema);
|
||||
|
||||
10967
api/package-lock.json
generated
10967
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chatgpt-clone",
|
||||
"version": "0.4.1",
|
||||
"name": "chat-backend",
|
||||
"version": "0.4.6",
|
||||
"description": "",
|
||||
"main": "server/index.js",
|
||||
"scripts": {
|
||||
@@ -21,7 +21,7 @@
|
||||
"dependencies": {
|
||||
"@dqbd/tiktoken": "^1.0.2",
|
||||
"@keyv/mongo": "^2.1.8",
|
||||
"@waylaidwanderer/chatgpt-api": "github:danny-avila/node-chatgpt-api",
|
||||
"@waylaidwanderer/chatgpt-api": "^1.36.0",
|
||||
"axios": "^1.3.4",
|
||||
"bcrypt": "^5.1.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
@@ -32,23 +32,24 @@
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "^8.36.0",
|
||||
"express": "^4.18.2",
|
||||
"googleapis": "^118.0.0",
|
||||
"handlebars": "^4.7.7",
|
||||
"html": "^1.0.0",
|
||||
"joi": "^14.3.1",
|
||||
"joi": "^17.9.2",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"keyv": "^4.5.2",
|
||||
"keyv-file": "^0.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"meilisearch": "^0.31.1",
|
||||
"mongoose": "^6.9.0",
|
||||
"meilisearch": "^0.32.3",
|
||||
"mongoose": "^7.1.1",
|
||||
"nodemailer": "^6.9.1",
|
||||
"og-chatgpt-api": "npm:@waylaidwanderer/chatgpt-api@^1.35.0",
|
||||
"openai": "^3.1.0",
|
||||
"passport": "^0.6.0",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"pino": "^8.12.1",
|
||||
"sanitize": "^2.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -3,28 +3,26 @@ const {
|
||||
logoutUser,
|
||||
registerUser,
|
||||
requestPasswordReset,
|
||||
resetPassword,
|
||||
} = require("../services/auth.service");
|
||||
resetPassword
|
||||
} = require('../services/auth.service');
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
const loginController = async (req, res) => {
|
||||
try {
|
||||
const token = req.user.generateToken();
|
||||
const user = await loginUser(req.user)
|
||||
if(user) {
|
||||
const user = await loginUser(req.user);
|
||||
if (user) {
|
||||
res.cookie('token', token, {
|
||||
expires: new Date(Date.now() + eval(process.env.SESSION_EXPIRY)),
|
||||
httpOnly: false,
|
||||
secure: isProduction
|
||||
});
|
||||
res.status(200).send({ token, user });
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return res.status(400).json({ message: 'Invalid credentials' });
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return res.status(500).json({ message: err.message });
|
||||
}
|
||||
@@ -35,22 +33,20 @@ const logoutController = async (req, res) => {
|
||||
const { refreshToken } = signedCookies;
|
||||
try {
|
||||
const logout = await logoutUser(req.user, refreshToken);
|
||||
console.log(logout)
|
||||
console.log(logout);
|
||||
const { status, message } = logout;
|
||||
if (status === 200) {
|
||||
res.clearCookie('token');
|
||||
res.clearCookie('refreshToken');
|
||||
res.status(status).send({ message });
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
res.status(status).send({ message });
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return res.status(500).json({ message: err.message });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const registrationController = async (req, res) => {
|
||||
try {
|
||||
@@ -65,13 +61,11 @@ const registrationController = async (req, res) => {
|
||||
secure: isProduction
|
||||
});
|
||||
res.status(status).send({ user });
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const { status, message } = response;
|
||||
res.status(status).send({ message });
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return res.status(500).json({ message: err.message });
|
||||
}
|
||||
@@ -83,17 +77,13 @@ const getUserController = async (req, res) => {
|
||||
|
||||
const resetPasswordRequestController = async (req, res) => {
|
||||
try {
|
||||
const resetService = await requestPasswordReset(
|
||||
req.body.email
|
||||
);
|
||||
const resetService = await requestPasswordReset(req.body.email);
|
||||
if (resetService.link) {
|
||||
return res.status(200).json(resetService);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return res.status(400).json(resetService);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return res.status(400).json({ message: e.message });
|
||||
}
|
||||
@@ -106,14 +96,12 @@ const resetPasswordController = async (req, res) => {
|
||||
req.body.token,
|
||||
req.body.password
|
||||
);
|
||||
if(resetPasswordService instanceof Error) {
|
||||
if (resetPasswordService instanceof Error) {
|
||||
return res.status(400).json(resetPasswordService);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return res.status(200).json(resetPasswordService);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return res.status(400).json({ message: e.message });
|
||||
}
|
||||
@@ -176,5 +164,5 @@ module.exports = {
|
||||
refreshController,
|
||||
registrationController,
|
||||
resetPasswordRequestController,
|
||||
resetPasswordController,
|
||||
};
|
||||
resetPasswordController
|
||||
};
|
||||
|
||||
@@ -10,8 +10,8 @@ const handleDuplicateKeyError = (err, res) => {
|
||||
//handle validation errors
|
||||
const handleValidationError = (err, res) => {
|
||||
console.log('congrats you hit the validation middleware');
|
||||
let errors = Object.values(err.errors).map(el => el.message);
|
||||
let fields = Object.values(err.errors).map(el => el.path);
|
||||
let errors = Object.values(err.errors).map((el) => el.message);
|
||||
let fields = Object.values(err.errors).map((el) => el.path);
|
||||
let code = 400;
|
||||
if (errors.length > 1) {
|
||||
const formattedErrors = errors.join(' ');
|
||||
|
||||
@@ -30,13 +30,13 @@ const projectPath = path.join(__dirname, '..', '..', 'client');
|
||||
app.use(passport.initialize());
|
||||
require('../strategies/jwtStrategy');
|
||||
require('../strategies/localStrategy');
|
||||
if(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
||||
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
||||
require('../strategies/googleStrategy');
|
||||
}
|
||||
if(process.env.FACEBOOK_CLIENT_ID && process.env.FACEBOOK_CLIENT_SECRET) {
|
||||
if (process.env.FACEBOOK_CLIENT_ID && process.env.FACEBOOK_CLIENT_SECRET) {
|
||||
require('../strategies/facebookStrategy');
|
||||
}
|
||||
app.use('/oauth', routes.oauth)
|
||||
app.use('/oauth', routes.oauth);
|
||||
// api endpoint
|
||||
app.use('/api/auth', routes.auth);
|
||||
app.use('/api/search', routes.search);
|
||||
@@ -48,8 +48,6 @@ const projectPath = path.join(__dirname, '..', '..', 'client');
|
||||
app.use('/api/tokenizer', routes.tokenizer);
|
||||
app.use('/api/endpoints', routes.endpoints);
|
||||
|
||||
|
||||
|
||||
// static files
|
||||
app.get('/*', function (req, res) {
|
||||
res.sendFile(path.join(projectPath, 'dist', 'index.html'));
|
||||
@@ -60,7 +58,8 @@ const projectPath = path.join(__dirname, '..', '..', 'client');
|
||||
console.log(
|
||||
`Server listening on all interface at port ${port}. Use http://localhost:${port} to access it`
|
||||
);
|
||||
else console.log(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`);
|
||||
else
|
||||
console.log(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`);
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
@@ -147,11 +147,13 @@ const ask = async ({
|
||||
const newConversationId = endpointOption?.jailbreak
|
||||
? response.jailbreakConversationId
|
||||
: response.conversationId || conversationId;
|
||||
const newUserMassageId = response.parentMessageId || response.details.requestId || userMessageId;
|
||||
const newUserMassageId =
|
||||
response.parentMessageId || response.details.requestId || userMessageId;
|
||||
const newResponseMessageId = response.messageId || response.details.messageId;
|
||||
|
||||
// STEP1 generate response message
|
||||
response.text = response.response || response.details.spokenText || '**Bing refused to answer.**';
|
||||
response.text =
|
||||
response.response || response.details.spokenText || '**Bing refused to answer.**';
|
||||
|
||||
let responseMessage = {
|
||||
conversationId: newConversationId,
|
||||
@@ -161,7 +163,8 @@ const ask = async ({
|
||||
sender: endpointOption?.jailbreak ? 'Sydney' : 'BingAI',
|
||||
text: await handleText(response, true),
|
||||
suggestions:
|
||||
response.details.suggestedResponses && response.details.suggestedResponses.map((s) => s.text),
|
||||
response.details.suggestedResponses &&
|
||||
response.details.suggestedResponses.map((s) => s.text),
|
||||
unfinished: false,
|
||||
cancelled: false,
|
||||
error: false
|
||||
@@ -215,7 +218,11 @@ const ask = async ({
|
||||
|
||||
// If response has parentMessageId, the fake userMessage.messageId should be updated to the real one.
|
||||
if (!overrideParentMessageId)
|
||||
await saveMessage({ ...userMessage, messageId: userMessageId, newMessageId: newUserMassageId });
|
||||
await saveMessage({
|
||||
...userMessage,
|
||||
messageId: userMessageId,
|
||||
newMessageId: newUserMassageId
|
||||
});
|
||||
userMessageId = newUserMassageId;
|
||||
|
||||
sendMessage(res, {
|
||||
@@ -228,7 +235,11 @@ const ask = async ({
|
||||
res.end();
|
||||
|
||||
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
|
||||
const title = await titleConvo({ endpoint: endpointOption?.endpoint, text, response: responseMessage });
|
||||
const title = await titleConvo({
|
||||
endpoint: endpointOption?.endpoint,
|
||||
text,
|
||||
response: responseMessage
|
||||
});
|
||||
|
||||
await saveConvo(req.user.id, {
|
||||
conversationId: conversationId,
|
||||
|
||||
@@ -180,7 +180,11 @@ const ask = async ({
|
||||
|
||||
// If response has parentMessageId, the fake userMessage.messageId should be updated to the real one.
|
||||
if (!overrideParentMessageId)
|
||||
await saveMessage({ ...userMessage, messageId: userMessageId, newMessageId: newUserMassageId });
|
||||
await saveMessage({
|
||||
...userMessage,
|
||||
messageId: userMessageId,
|
||||
newMessageId: newUserMassageId
|
||||
});
|
||||
userMessageId = newUserMassageId;
|
||||
|
||||
sendMessage(res, {
|
||||
|
||||
156
api/server/routes/ask/askGoogle.js
Normal file
156
api/server/routes/ask/askGoogle.js
Normal file
@@ -0,0 +1,156 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { titleConvo } = require('../../../app/');
|
||||
const GoogleClient = require('../../../app/google/GoogleClient');
|
||||
const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('../../../models');
|
||||
const { handleError, sendMessage, createOnProgress } = require('./handlers');
|
||||
const requireJwtAuth = require('../../../middleware/requireJwtAuth');
|
||||
|
||||
router.post('/', requireJwtAuth, async (req, res) => {
|
||||
const { endpoint, text, parentMessageId, conversationId } = req.body;
|
||||
if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' });
|
||||
if (endpoint !== 'google') return handleError(res, { text: 'Illegal request' });
|
||||
|
||||
// build endpoint option
|
||||
const endpointOption = {
|
||||
examples: req.body?.examples ?? [{ input: { content: '' }, output: { content: '' } }],
|
||||
promptPrefix: req.body?.promptPrefix ?? null,
|
||||
token: req.body?.token ?? null,
|
||||
modelOptions: {
|
||||
model: req.body?.model ?? 'chat-bison',
|
||||
modelLabel: req.body?.modelLabel ?? null,
|
||||
temperature: req.body?.temperature ?? 0.2,
|
||||
maxOutputTokens: req.body?.maxOutputTokens ?? 1024,
|
||||
topP: req.body?.topP ?? 0.95,
|
||||
topK: req.body?.topK ?? 40
|
||||
}
|
||||
};
|
||||
|
||||
const availableModels = ['chat-bison', 'text-bison'];
|
||||
if (availableModels.find((model) => model === endpointOption.modelOptions.model) === undefined) {
|
||||
return handleError(res, { text: `Illegal request: model` });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
return await ask({
|
||||
text,
|
||||
endpointOption,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
req,
|
||||
res
|
||||
});
|
||||
});
|
||||
|
||||
const ask = async ({ text, endpointOption, parentMessageId = null, conversationId, req, res }) => {
|
||||
res.writeHead(200, {
|
||||
Connection: 'keep-alive',
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache, no-transform',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'X-Accel-Buffering': 'no'
|
||||
});
|
||||
let userMessage;
|
||||
let userMessageId;
|
||||
let responseMessageId;
|
||||
let lastSavedTimestamp = 0;
|
||||
|
||||
try {
|
||||
const getIds = (data) => {
|
||||
userMessage = data.userMessage;
|
||||
userMessageId = userMessage.messageId;
|
||||
responseMessageId = data.responseMessageId;
|
||||
if (!conversationId) {
|
||||
conversationId = data.conversationId;
|
||||
}
|
||||
};
|
||||
|
||||
const { onProgress: progressCallback } = createOnProgress({
|
||||
onProgress: ({ text: partialText }) => {
|
||||
const currentTimestamp = Date.now();
|
||||
if (currentTimestamp - lastSavedTimestamp > 500) {
|
||||
lastSavedTimestamp = currentTimestamp;
|
||||
saveMessage({
|
||||
messageId: responseMessageId,
|
||||
sender: 'PaLM2',
|
||||
conversationId,
|
||||
parentMessageId: userMessageId,
|
||||
text: partialText,
|
||||
unfinished: true,
|
||||
cancelled: false,
|
||||
error: false
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const abortController = new AbortController();
|
||||
|
||||
let key;
|
||||
if (endpointOption.token) {
|
||||
key = JSON.parse(endpointOption.token);
|
||||
delete endpointOption.token;
|
||||
console.log('Using service account key provided by User for PaLM models');
|
||||
}
|
||||
|
||||
try {
|
||||
if (!key) {
|
||||
key = require('../../../data/auth.json');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("No 'auth.json' file (service account key) found in /api/data/ for PaLM models");
|
||||
}
|
||||
|
||||
const clientOptions = {
|
||||
// debug: true, // for testing
|
||||
reverseProxyUrl: process.env.GOOGLE_REVERSE_PROXY || null,
|
||||
proxy: process.env.PROXY || null,
|
||||
...endpointOption
|
||||
};
|
||||
|
||||
const client = new GoogleClient(key, clientOptions);
|
||||
|
||||
let response = await client.sendMessage(text, {
|
||||
getIds,
|
||||
user: req.user.id,
|
||||
parentMessageId,
|
||||
conversationId,
|
||||
onProgress: progressCallback.call(null, { res, text, parentMessageId: userMessageId }),
|
||||
abortController
|
||||
});
|
||||
|
||||
await saveMessage(response);
|
||||
sendMessage(res, {
|
||||
title: await getConvoTitle(req.user.id, conversationId),
|
||||
final: true,
|
||||
conversation: await getConvo(req.user.id, conversationId),
|
||||
requestMessage: userMessage,
|
||||
responseMessage: response
|
||||
});
|
||||
res.end();
|
||||
|
||||
if (parentMessageId == '00000000-0000-0000-0000-000000000000') {
|
||||
const title = await titleConvo({ text, response });
|
||||
await saveConvo(req.user.id, {
|
||||
conversationId: conversationId,
|
||||
title
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const errorMessage = {
|
||||
messageId: responseMessageId,
|
||||
sender: 'PaLM2',
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
unfinished: false,
|
||||
cancelled: false,
|
||||
error: true,
|
||||
text: error.message
|
||||
};
|
||||
await saveMessage(errorMessage);
|
||||
handleError(res, errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = router;
|
||||
@@ -64,7 +64,7 @@ router.post('/', requireJwtAuth, async (req, res) => {
|
||||
};
|
||||
|
||||
const availableModels = getOpenAIModels();
|
||||
if (availableModels.find(model => model === endpointOption.model) === undefined)
|
||||
if (availableModels.find((model) => model === endpointOption.model) === undefined)
|
||||
return handleError(res, { text: 'Illegal request: model' });
|
||||
|
||||
console.log('ask log', {
|
||||
@@ -169,11 +169,13 @@ const ask = async ({
|
||||
};
|
||||
const abortKey = conversationId;
|
||||
abortControllers.set(abortKey, { abortController, ...endpointOption });
|
||||
const oaiApiKey = req.body?.token ?? null;
|
||||
|
||||
let response = await askClient({
|
||||
text,
|
||||
parentMessageId: userParentMessageId,
|
||||
conversationId,
|
||||
oaiApiKey,
|
||||
...endpointOption,
|
||||
onProgress: progressCallback.call(null, {
|
||||
res,
|
||||
@@ -236,7 +238,11 @@ const ask = async ({
|
||||
|
||||
// If response has parentMessageId, the fake userMessage.messageId should be updated to the real one.
|
||||
if (!overrideParentMessageId)
|
||||
await saveMessage({ ...userMessage, messageId: userMessageId, newMessageId: newUserMassageId });
|
||||
await saveMessage({
|
||||
...userMessage,
|
||||
messageId: userMessageId,
|
||||
newMessageId: newUserMassageId
|
||||
});
|
||||
userMessageId = newUserMassageId;
|
||||
|
||||
sendMessage(res, {
|
||||
@@ -249,7 +255,12 @@ const ask = async ({
|
||||
res.end();
|
||||
|
||||
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
|
||||
const title = await titleConvo({ endpoint: endpointOption?.endpoint, text, response: responseMessage });
|
||||
const title = await titleConvo({
|
||||
endpoint: endpointOption?.endpoint,
|
||||
text,
|
||||
response: responseMessage,
|
||||
oaiApiKey
|
||||
});
|
||||
await saveConvo(req.user.id, {
|
||||
conversationId: conversationId,
|
||||
title
|
||||
|
||||
@@ -2,11 +2,13 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
// const askAzureOpenAI = require('./askAzureOpenAI';)
|
||||
const askOpenAI = require('./askOpenAI');
|
||||
const askGoogle = require('./askGoogle');
|
||||
const askBingAI = require('./askBingAI');
|
||||
const askChatGPTBrowser = require('./askChatGPTBrowser');
|
||||
|
||||
// router.use('/azureOpenAI', askAzureOpenAI);
|
||||
router.use('/openAI', askOpenAI);
|
||||
router.use('/google', askGoogle);
|
||||
router.use('/bingAI', askBingAI);
|
||||
router.use('/chatGPTBrowser', askChatGPTBrowser);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ const {
|
||||
loginController,
|
||||
logoutController,
|
||||
refreshController,
|
||||
registrationController,
|
||||
registrationController
|
||||
} = require('../controllers/auth.controller');
|
||||
const requireJwtAuth = require('../../middleware/requireJwtAuth');
|
||||
const requireLocalAuth = require('../../middleware/requireLocalAuth');
|
||||
|
||||
@@ -9,15 +9,41 @@ const getOpenAIModels = () => {
|
||||
};
|
||||
|
||||
const getChatGPTBrowserModels = () => {
|
||||
let models = ['text-davinci-002-render-sha', 'text-davinci-002-render-paid', 'gpt-4'];
|
||||
let models = ['text-davinci-002-render-sha', 'gpt-4'];
|
||||
if (process.env.CHATGPT_MODELS) models = String(process.env.CHATGPT_MODELS).split(',');
|
||||
|
||||
return models;
|
||||
};
|
||||
|
||||
router.get('/', function (req, res) {
|
||||
let i = 0;
|
||||
router.get('/', async function (req, res) {
|
||||
let key, palmUser;
|
||||
try {
|
||||
key = require('../../data/auth.json');
|
||||
} catch (e) {
|
||||
if (i === 0) {
|
||||
console.log("No 'auth.json' file (service account key) found in /api/data/ for PaLM models");
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.PALM_KEY === 'user_provided') {
|
||||
palmUser = true;
|
||||
if (i <= 1) {
|
||||
console.log('User will provide key for PaLM models');
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
const google =
|
||||
key || palmUser
|
||||
? { userProvide: palmUser, availableModels: ['chat-bison', 'text-bison'] }
|
||||
: false;
|
||||
const azureOpenAI = !!process.env.AZURE_OPENAI_KEY;
|
||||
const openAI = process.env.OPENAI_KEY ? { availableModels: getOpenAIModels() } : false;
|
||||
const apiKey = process.env.OPENAI_KEY || process.env.AZURE_OPENAI_API_KEY;
|
||||
const openAI = apiKey
|
||||
? { availableModels: getOpenAIModels(), userProvide: apiKey === 'user_provided' }
|
||||
: false;
|
||||
const bingAI = process.env.BINGAI_TOKEN
|
||||
? { userProvide: process.env.BINGAI_TOKEN == 'user_provided' }
|
||||
: false;
|
||||
@@ -28,7 +54,7 @@ router.get('/', function (req, res) {
|
||||
}
|
||||
: false;
|
||||
|
||||
res.send(JSON.stringify({ azureOpenAI, openAI, bingAI, chatGPTBrowser }));
|
||||
res.send(JSON.stringify({ azureOpenAI, openAI, google, bingAI, chatGPTBrowser }));
|
||||
});
|
||||
|
||||
module.exports = { router, getOpenAIModels, getChatGPTBrowserModels };
|
||||
|
||||
@@ -19,5 +19,5 @@ module.exports = {
|
||||
auth,
|
||||
oauth,
|
||||
tokenizer,
|
||||
endpoints,
|
||||
endpoints
|
||||
};
|
||||
|
||||
@@ -61,4 +61,4 @@ router.get(
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
|
||||
@@ -40,7 +40,7 @@ router.post('/delete', requireJwtAuth, async (req, res) => {
|
||||
try {
|
||||
await deletePresets(req.user.id, filter);
|
||||
|
||||
const presets = (await getPresets(req.user.id)).map(preset => preset.toObject());
|
||||
const presets = (await getPresets(req.user.id)).map((preset) => preset.toObject());
|
||||
|
||||
// console.log('delete preset response', presets);
|
||||
res.status(201).send(presets);
|
||||
|
||||
@@ -27,7 +27,9 @@ router.get('/', requireJwtAuth, async function (req, res) {
|
||||
console.log('cache hit', key);
|
||||
const cached = cache.get(key);
|
||||
const { pages, pageSize, messages } = cached;
|
||||
res.status(200).send({ conversations: cached[pageNumber], pages, pageNumber, pageSize, messages });
|
||||
res
|
||||
.status(200)
|
||||
.send({ conversations: cached[pageNumber], pages, pageNumber, pageSize, messages });
|
||||
return;
|
||||
} else {
|
||||
cache.clear();
|
||||
@@ -44,7 +46,7 @@ router.get('/', requireJwtAuth, async function (req, res) {
|
||||
},
|
||||
true
|
||||
)
|
||||
).hits.map(message => {
|
||||
).hits.map((message) => {
|
||||
const { _formatted, ...rest } = message;
|
||||
return {
|
||||
...rest,
|
||||
@@ -95,12 +97,12 @@ router.get('/clear', async function (req, res) {
|
||||
|
||||
router.get('/test', async function (req, res) {
|
||||
const { q } = req.query;
|
||||
const messages = (await Message.meiliSearch(q, { attributesToHighlight: ['text'] }, true)).hits.map(
|
||||
message => {
|
||||
const { _formatted, ...rest } = message;
|
||||
return { ...rest, searchResult: true, text: _formatted.text };
|
||||
}
|
||||
);
|
||||
const messages = (
|
||||
await Message.meiliSearch(q, { attributesToHighlight: ['text'] }, true)
|
||||
).hits.map((message) => {
|
||||
const { _formatted, ...rest } = message;
|
||||
return { ...rest, searchResult: true, text: _formatted.text };
|
||||
});
|
||||
res.send(messages);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ const sendEmail = require('../../utils/sendEmail');
|
||||
const crypto = require('crypto');
|
||||
const bcrypt = require('bcrypt');
|
||||
const DebugControl = require('../../utils/debug.js');
|
||||
const Joi = require('joi');
|
||||
const { registerSchema } = require('../../strategies/validators');
|
||||
const migrateDataToFirstUser = require('../../utils/migrateDataToFirstUser');
|
||||
|
||||
@@ -25,29 +24,26 @@ const loginUser = async (user) => {
|
||||
};
|
||||
|
||||
const logoutUser = async (user, refreshToken) => {
|
||||
User.findById(user._id).then((user) => {
|
||||
const tokenIndex = user.refreshToken.findIndex(item => item.refreshToken === refreshToken);
|
||||
try {
|
||||
const userFound = await User.findById(user._id);
|
||||
const tokenIndex = userFound.refreshToken.findIndex((item) => item.refreshToken === refreshToken);
|
||||
|
||||
if (tokenIndex !== -1) {
|
||||
user.refreshToken.id(user.refreshToken[tokenIndex]._id).remove();
|
||||
userFound.refreshToken.id(userFound.refreshToken[tokenIndex]._id).remove();
|
||||
}
|
||||
|
||||
user.save((err) => {
|
||||
if (err) {
|
||||
return { status: 500, message: err.message };
|
||||
} else {
|
||||
//res.clearCookie('refreshToken', COOKIE_OPTIONS);
|
||||
// removeTokenCookie(res);
|
||||
return { status: 200, message: 'Logout successful' };
|
||||
}
|
||||
});
|
||||
});
|
||||
return { status: 200, message: 'Logout successful' };
|
||||
};
|
||||
await userFound.save();
|
||||
//res.clearCookie('refreshToken', COOKIE_OPTIONS);
|
||||
// removeTokenCookie(res);
|
||||
return { status: 200, message: 'Logout successful' };
|
||||
} catch (err) {
|
||||
return { status: 500, message: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
const registerUser = async (user) => {
|
||||
let response = {};
|
||||
const { error } = Joi.validate(user, registerSchema);
|
||||
const { error } = registerSchema.validate(user);
|
||||
if (error) {
|
||||
log({
|
||||
title: 'Route: register - Joi Validation Error',
|
||||
@@ -78,7 +74,7 @@ const registerUser = async (user) => {
|
||||
}
|
||||
|
||||
//determine if this is the first registered user (not counting anonymous_user)
|
||||
const isFirstRegisteredUser = await User.countDocuments({}) === 0;
|
||||
const isFirstRegisteredUser = (await User.countDocuments({})) === 0;
|
||||
|
||||
try {
|
||||
const newUser = await new User({
|
||||
@@ -88,7 +84,7 @@ const registerUser = async (user) => {
|
||||
username,
|
||||
name,
|
||||
avatar: null,
|
||||
role: isFirstRegisteredUser ? 'ADMIN' : 'USER',
|
||||
role: isFirstRegisteredUser ? 'ADMIN' : 'USER'
|
||||
});
|
||||
|
||||
// todo: implement refresh token
|
||||
@@ -104,7 +100,7 @@ const registerUser = async (user) => {
|
||||
newUser.save();
|
||||
});
|
||||
});
|
||||
console.log('newUser', newUser)
|
||||
console.log('newUser', newUser);
|
||||
if (isFirstRegisteredUser) {
|
||||
migrateDataToFirstUser(newUser);
|
||||
// console.log(migrate);
|
||||
@@ -186,12 +182,11 @@ const resetPassword = async (userId, token, password) => {
|
||||
return { message: 'Password reset was successful' };
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
// signup,
|
||||
registerUser,
|
||||
loginUser,
|
||||
logoutUser,
|
||||
requestPasswordReset,
|
||||
resetPassword,
|
||||
resetPassword
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const passport = require('passport');
|
||||
const FacebookStrategy = require('passport-facebook').Strategy;
|
||||
const passport = require('passport');
|
||||
const FacebookStrategy = require('passport-facebook').Strategy;
|
||||
const User = require('../models/User');
|
||||
|
||||
|
||||
const serverUrl =
|
||||
process.env.NODE_ENV === 'production' ? process.env.SERVER_URL_PROD : process.env.SERVER_URL_DEV;
|
||||
|
||||
@@ -11,7 +11,7 @@ const facebookLogin = new FacebookStrategy(
|
||||
clientID: process.env.FACEBOOK_APP_ID,
|
||||
clientSecret: process.env.FACEBOOK_SECRET,
|
||||
callbackURL: `${serverUrl}${process.env.FACEBOOK_CALLBACK_URL}`,
|
||||
proxy: true,
|
||||
proxy: true
|
||||
// profileFields: [
|
||||
// 'id',
|
||||
// 'email',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const passport = require('passport');
|
||||
const PassportLocalStrategy = require('passport-local').Strategy;
|
||||
const Joi = require('joi');
|
||||
|
||||
const User = require('../models/User');
|
||||
const { loginSchema } = require('./validators');
|
||||
@@ -14,7 +13,7 @@ const passportLogin = new PassportLocalStrategy(
|
||||
passReqToCallback: true
|
||||
},
|
||||
async (req, email, password, done) => {
|
||||
const { error } = Joi.validate(req.body, loginSchema);
|
||||
const { error } = loginSchema.validate(req.body);
|
||||
if (error) {
|
||||
log({
|
||||
title: 'Passport Local Strategy - Validation Error',
|
||||
@@ -65,4 +64,3 @@ function log({ title, parameters }) {
|
||||
DebugControl.log.parameters(parameters);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,4 +21,4 @@ const registerSchema = Joi.object().keys({
|
||||
module.exports = {
|
||||
loginSchema,
|
||||
registerSchema
|
||||
};
|
||||
};
|
||||
|
||||
125
api/utils/LoggingSystem.js
Normal file
125
api/utils/LoggingSystem.js
Normal file
@@ -0,0 +1,125 @@
|
||||
const pino = require('pino');
|
||||
|
||||
const logger = pino({
|
||||
level: 'info',
|
||||
redact: {
|
||||
paths: [
|
||||
// List of Paths to redact from the logs (https://getpino.io/#/docs/redaction)
|
||||
'env.OPENAI_KEY',
|
||||
'env.BINGAI_TOKEN',
|
||||
'env.CHATGPT_TOKEN',
|
||||
'env.MEILI_MASTER_KEY',
|
||||
'env.GOOGLE_CLIENT_SECRET',
|
||||
'env.JWT_SECRET_DEV',
|
||||
'env.JWT_SECRET_PROD',
|
||||
'newUser.password'
|
||||
], // See example to filter object class instances
|
||||
censor: '***' // Redaction character
|
||||
}
|
||||
});
|
||||
|
||||
// Sanitize outside the logger paths. This is useful for sanitizing variables directly with Regex and patterns.
|
||||
const redactPatterns = [
|
||||
// Array of regular expressions for redacting patterns
|
||||
/api[-_]?key/i,
|
||||
/password/i,
|
||||
/token/i,
|
||||
/secret/i,
|
||||
/key/i,
|
||||
/certificate/i,
|
||||
/client[-_]?id/i,
|
||||
/authorization[-_]?code/i,
|
||||
/authorization[-_]?login[-_]?hint/i,
|
||||
/authorization[-_]?acr[-_]?values/i,
|
||||
/authorization[-_]?response[-_]?mode/i,
|
||||
/authorization[-_]?nonce/i
|
||||
];
|
||||
|
||||
/*
|
||||
// Example of redacting sensitive data from object class instances
|
||||
function redactSensitiveData(obj) {
|
||||
if (obj instanceof User) {
|
||||
return {
|
||||
...obj.toObject(),
|
||||
password: '***', // Redact the password field
|
||||
};
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Example of redacting sensitive data from object class instances
|
||||
logger.info({ newUser: redactSensitiveData(newUser) }, 'newUser');
|
||||
*/
|
||||
|
||||
const levels = {
|
||||
TRACE: 10,
|
||||
DEBUG: 20,
|
||||
INFO: 30,
|
||||
WARN: 40,
|
||||
ERROR: 50,
|
||||
FATAL: 60
|
||||
};
|
||||
|
||||
let level = levels.INFO;
|
||||
|
||||
module.exports = {
|
||||
levels,
|
||||
setLevel: (l) => (level = l),
|
||||
log: {
|
||||
trace: (msg) => {
|
||||
if (level <= levels.TRACE) return;
|
||||
logger.trace(msg);
|
||||
},
|
||||
debug: (msg) => {
|
||||
if (level <= levels.DEBUG) return;
|
||||
logger.debug(msg);
|
||||
},
|
||||
info: (msg) => {
|
||||
if (level <= levels.INFO) return;
|
||||
logger.info(msg);
|
||||
},
|
||||
warn: (msg) => {
|
||||
if (level <= levels.WARN) return;
|
||||
logger.warn(msg);
|
||||
},
|
||||
error: (msg) => {
|
||||
if (level <= levels.ERROR) return;
|
||||
logger.error(msg);
|
||||
},
|
||||
fatal: (msg) => {
|
||||
if (level <= levels.FATAL) return;
|
||||
logger.fatal(msg);
|
||||
},
|
||||
|
||||
// Custom loggers
|
||||
parameters: (parameters) => {
|
||||
if (level <= levels.TRACE) return;
|
||||
logger.debug({ parameters }, 'Function Parameters');
|
||||
},
|
||||
functionName: (name) => {
|
||||
if (level <= levels.TRACE) return;
|
||||
logger.debug(`EXECUTING: ${name}`);
|
||||
},
|
||||
flow: (flow) => {
|
||||
if (level <= levels.INFO) return;
|
||||
logger.debug(`BEGIN FLOW: ${flow}`);
|
||||
},
|
||||
variable: ({ name, value }) => {
|
||||
if (level <= levels.DEBUG) return;
|
||||
// Check if the variable name matches any of the redact patterns and redact the value
|
||||
let sanitizedValue = value;
|
||||
for (const pattern of redactPatterns) {
|
||||
if (pattern.test(name)) {
|
||||
sanitizedValue = '***';
|
||||
break;
|
||||
}
|
||||
}
|
||||
logger.debug({ variable: { name, value: sanitizedValue } }, `VARIABLE ${name}`);
|
||||
},
|
||||
request: () => (req, res, next) => {
|
||||
if (level < levels.DEBUG) return next();
|
||||
logger.debug({ query: req.query, body: req.body }, `Hit URL ${req.url} with following`);
|
||||
return next();
|
||||
}
|
||||
}
|
||||
};
|
||||
9
api/utils/genAzureEndpoints.js
Normal file
9
api/utils/genAzureEndpoints.js
Normal file
@@ -0,0 +1,9 @@
|
||||
function genAzureEndpoint({
|
||||
azureOpenAIApiInstanceName,
|
||||
azureOpenAIApiDeploymentName,
|
||||
azureOpenAIApiVersion
|
||||
}) {
|
||||
return `https://${azureOpenAIApiInstanceName}.openai.azure.com/openai/deployments/${azureOpenAIApiDeploymentName}/chat/completions?api-version=${azureOpenAIApiVersion}`;
|
||||
}
|
||||
|
||||
module.exports = { genAzureEndpoint };
|
||||
@@ -3,28 +3,27 @@ const Preset = require('../models/schema/presetSchema');
|
||||
|
||||
const migrateConversations = async (userId) => {
|
||||
try {
|
||||
return await Conversation.updateMany({ user: null }, { $set: { user: userId }}).exec();
|
||||
return await Conversation.updateMany({ user: null }, { $set: { user: userId } }).exec();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error saving conversation' };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const migratePresets = async (userId) => {
|
||||
try {
|
||||
return await Preset.updateMany({ user: null }, { $set: { user: userId }}).exec();
|
||||
return await Preset.updateMany({ user: null }, { $set: { user: userId } }).exec();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error saving conversation' };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const migrateDataToFirstUser = async (user) => {
|
||||
const conversations = await migrateConversations(user.id);
|
||||
console.log(conversations);
|
||||
const presets = await migratePresets(user.id);
|
||||
console.log(presets);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
module.exports = migrateDataToFirstUser;
|
||||
module.exports = migrateDataToFirstUser;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const nodemailer = require("nodemailer");
|
||||
const handlebars = require("handlebars");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const nodemailer = require('nodemailer');
|
||||
const handlebars = require('handlebars');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const sendEmail = async (email, subject, payload, template) => {
|
||||
try {
|
||||
@@ -11,18 +11,18 @@ const sendEmail = async (email, subject, payload, template) => {
|
||||
port: 465,
|
||||
auth: {
|
||||
user: process.env.EMAIL_USERNAME,
|
||||
pass: process.env.EMAIL_PASSWORD,
|
||||
},
|
||||
pass: process.env.EMAIL_PASSWORD
|
||||
}
|
||||
});
|
||||
|
||||
const source = fs.readFileSync(path.join(__dirname, template), "utf8");
|
||||
const source = fs.readFileSync(path.join(__dirname, template), 'utf8');
|
||||
const compiledTemplate = handlebars.compile(source);
|
||||
const options = () => {
|
||||
return {
|
||||
from: process.env.FROM_EMAIL,
|
||||
to: email,
|
||||
subject: subject,
|
||||
html: compiledTemplate(payload),
|
||||
html: compiledTemplate(payload)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ const sendEmail = async (email, subject, payload, template) => {
|
||||
return error;
|
||||
} else {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
success: true
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -51,4 +51,4 @@ sendEmail(
|
||||
);
|
||||
*/
|
||||
|
||||
module.exports = sendEmail;
|
||||
module.exports = sendEmail;
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
###########################
|
||||
# App configuration:
|
||||
###########################
|
||||
|
||||
# Custom app name, this text will be displayed in the landing page and the footer.
|
||||
VITE_APP_TITLE="ChatGPT Clone"
|
||||
|
||||
###########################
|
||||
# Server URL configuration:
|
||||
###########################
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true,
|
||||
"commonjs": true,
|
||||
"es6": true,
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"overrides": [
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
'react/prop-types': ['off'],
|
||||
'react/display-name': ['off'],
|
||||
"no-debugger":"off",
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"singleAttributePerLine": true,
|
||||
"bracketSameLine": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"printWidth": 110,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"parser": "babel"
|
||||
}
|
||||
19932
client/package-lock.json
generated
19932
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chatgpt-clone",
|
||||
"version": "0.4.1",
|
||||
"name": "chat-frontend",
|
||||
"version": "0.4.6",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -31,6 +31,7 @@
|
||||
"@radix-ui/react-dialog": "^1.0.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.2",
|
||||
"@radix-ui/react-hover-card": "^1.0.5",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.0",
|
||||
"@radix-ui/react-slider": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.0.3",
|
||||
@@ -42,24 +43,25 @@
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@zattoo/use-double-click": "1.2.0",
|
||||
"axios": "^1.3.4",
|
||||
"class-variance-authority": "^0.4.0",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"clsx": "^1.2.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"downloadjs": "^1.4.7",
|
||||
"esbuild": "0.17.15",
|
||||
"export-from-json": "^1.7.2",
|
||||
"filenamify": "^5.1.1",
|
||||
"filenamify": "^6.0.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.113.0",
|
||||
"lucide-react": "^0.220.0",
|
||||
"pino": "^8.12.1",
|
||||
"rc-input-number": "^7.4.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-lazy-load": "^4.0.1",
|
||||
"react-markdown": "^8.0.6",
|
||||
"react-router-dom": "^6.9.0",
|
||||
"react-router-dom": "^6.11.2",
|
||||
"react-string-replace": "^1.1.0",
|
||||
"react-textarea-autosize": "^8.4.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
@@ -96,23 +98,15 @@
|
||||
"babel-plugin-root-import": "^6.6.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"css-loader": "^6.7.3",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"path": "^0.12.7",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-loader": "^7.1.0",
|
||||
"postcss-preset-env": "^8.2.0",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||
"source-map-loader": "^1.1.3",
|
||||
"source-map-loader": "^4.0.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"tailwindcss": "^3.2.6",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.5",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.2.1",
|
||||
"vite-plugin-html": "^3.2.0"
|
||||
}
|
||||
|
||||
BIN
client/public/assets/palm.png
Normal file
BIN
client/public/assets/palm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
client/public/fonts/signifier-bold-italic.woff2
Normal file
BIN
client/public/fonts/signifier-bold-italic.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/signifier-bold.woff2
Normal file
BIN
client/public/fonts/signifier-bold.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/signifier-light-italic.woff2
Normal file
BIN
client/public/fonts/signifier-light-italic.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/signifier-light.woff2
Normal file
BIN
client/public/fonts/signifier-light.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/soehne-buch-kursiv.woff2
Normal file
BIN
client/public/fonts/soehne-buch-kursiv.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/soehne-buch.woff2
Normal file
BIN
client/public/fonts/soehne-buch.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/soehne-halbfett-kursiv.woff2
Normal file
BIN
client/public/fonts/soehne-halbfett-kursiv.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/soehne-halbfett.woff2
Normal file
BIN
client/public/fonts/soehne-halbfett.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/soehne-kraftig-kursiv.woff2
Normal file
BIN
client/public/fonts/soehne-kraftig-kursiv.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/soehne-kraftig.woff2
Normal file
BIN
client/public/fonts/soehne-kraftig.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/soehne-mono-buch-kursiv.woff2
Normal file
BIN
client/public/fonts/soehne-mono-buch-kursiv.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/soehne-mono-buch.woff2
Normal file
BIN
client/public/fonts/soehne-mono-buch.woff2
Normal file
Binary file not shown.
BIN
client/public/fonts/soehne-mono-halbfett.woff2
Normal file
BIN
client/public/fonts/soehne-mono-halbfett.woff2
Normal file
Binary file not shown.
@@ -44,12 +44,7 @@ const router = createBrowserRouter([
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: (
|
||||
<Navigate
|
||||
to="/chat/new"
|
||||
replace={true}
|
||||
/>
|
||||
)
|
||||
element: <Navigate to="/chat/new" replace={true} />
|
||||
},
|
||||
{
|
||||
path: 'chat/:conversationId?',
|
||||
@@ -70,7 +65,7 @@ const App = () => {
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
queryCache: new QueryCache({
|
||||
onError: error => {
|
||||
onError: (error) => {
|
||||
if (error?.response?.status === 401) {
|
||||
setError(error);
|
||||
}
|
||||
|
||||
@@ -1,43 +1,41 @@
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { TLoginUser } from "~/data-provider";
|
||||
import { useAuthContext } from "~/hooks/AuthContext";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { TLoginUser } from '~/data-provider';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
function Login() {
|
||||
const { login, error, isAuthenticated } = useAuthContext();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
formState: { errors }
|
||||
} = useForm<TLoginUser>();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
navigate("/chat/new");
|
||||
navigate('/chat/new');
|
||||
}
|
||||
}, [isAuthenticated, navigate])
|
||||
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
const SERVER_URL = import.meta.env.DEV
|
||||
? import.meta.env.VITE_SERVER_URL_DEV
|
||||
: import.meta.env.VITE_SERVER_URL_PROD;
|
||||
const showGoogleLogin =
|
||||
import.meta.env.VITE_SHOW_GOOGLE_LOGIN_OPTION === "true";
|
||||
const showGoogleLogin = import.meta.env.VITE_SHOW_GOOGLE_LOGIN_OPTION === 'true';
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center pt-6 justify-center sm:pt-0 bg-white">
|
||||
<div className="mt-6 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg w-96">
|
||||
<h1 className="text-center text-3xl font-semibold mb-4">Welcome back</h1>
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
||||
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
||||
<h1 className="mb-4 text-center text-3xl font-semibold">Welcome back</h1>
|
||||
{error && (
|
||||
<div
|
||||
className="mt-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"
|
||||
className="relative mt-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"
|
||||
role="alert"
|
||||
>
|
||||
Unable to login with the information provided. Please check your
|
||||
credentials and try again.
|
||||
Unable to login with the information provided. Please check your credentials and try
|
||||
again.
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
@@ -53,28 +51,28 @@ function Login() {
|
||||
id="email"
|
||||
autoComplete="email"
|
||||
aria-label="Email"
|
||||
{...register("email", {
|
||||
required: "Email is required",
|
||||
{...register('email', {
|
||||
required: 'Email is required',
|
||||
minLength: {
|
||||
value: 3,
|
||||
message: "Email must be at least 6 characters",
|
||||
message: 'Email must be at least 6 characters'
|
||||
},
|
||||
maxLength: {
|
||||
value: 120,
|
||||
message: "Email should not be longer than 120 characters",
|
||||
message: 'Email should not be longer than 120 characters'
|
||||
},
|
||||
pattern: {
|
||||
value: /\S+@\S+\.\S+/,
|
||||
message: "You must enter a valid email address",
|
||||
},
|
||||
message: 'You must enter a valid email address'
|
||||
}
|
||||
})}
|
||||
aria-invalid={!!errors.email}
|
||||
className="block rounded-t-md px-2.5 pb-2.5 pt-5 w-full text-sm text-gray-900 bg-gray-50 border-0 border-b-2 border-gray-300 appearance-none focus:outline-none focus:ring-0 focus:border-green-500 peer"
|
||||
className="peer block w-full appearance-none rounded-t-md border-0 border-b-2 border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
||||
placeholder=" "
|
||||
></input>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="absolute text-gray-500 duration-300 transform -translate-y-4 scale-75 top-4 z-10 origin-[0] left-2.5 peer-focus:text-green-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-4"
|
||||
className="absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-gray-500 duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
||||
>
|
||||
Email address
|
||||
</label>
|
||||
@@ -93,24 +91,24 @@ function Login() {
|
||||
id="password"
|
||||
autoComplete="current-password"
|
||||
aria-label="Password"
|
||||
{...register("password", {
|
||||
required: "Password is required",
|
||||
{...register('password', {
|
||||
required: 'Password is required',
|
||||
minLength: {
|
||||
value: 8,
|
||||
message: "Password must be at least 8 characters",
|
||||
message: 'Password must be at least 8 characters'
|
||||
},
|
||||
maxLength: {
|
||||
value: 40,
|
||||
message: "Password must be less than 40 characters",
|
||||
},
|
||||
message: 'Password must be less than 40 characters'
|
||||
}
|
||||
})}
|
||||
aria-invalid={!!errors.password}
|
||||
className="block rounded-t-md px-2.5 pb-2.5 pt-5 w-full text-sm text-gray-900 bg-gray-50 border-0 border-b-2 border-gray-300 appearance-none focus:outline-none focus:ring-0 focus:border-green-500 peer"
|
||||
className="peer block w-full appearance-none rounded-t-md border-0 border-b-2 border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
||||
placeholder=" "
|
||||
></input>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="absolute text-gray-500 duration-300 transform -translate-y-4 scale-75 top-4 z-10 origin-[0] left-2.5 peer-focus:text-green-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-4"
|
||||
className="absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-gray-500 duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
@@ -123,10 +121,7 @@ function Login() {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<a
|
||||
href="/forgot-password"
|
||||
className="text-sm text-green-500 hover:underline"
|
||||
>
|
||||
<a href="/forgot-password" className="text-sm text-green-500 hover:underline">
|
||||
Forgot Password?
|
||||
</a>
|
||||
<div className="mt-6">
|
||||
@@ -139,28 +134,47 @@ function Login() {
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<p className="my-4 text-center text-sm font-light text-gray-700">
|
||||
{" "}
|
||||
Don't have an account?{" "}
|
||||
<a
|
||||
href="/register"
|
||||
className="p-1 text-green-500 hover:underline"
|
||||
>
|
||||
<p className="my-4 text-center text-sm font-light text-gray-700">
|
||||
{' '}
|
||||
Don't have an account?{' '}
|
||||
<a href="/register" className="p-1 text-green-500 hover:underline">
|
||||
Sign up
|
||||
</a>
|
||||
</p>
|
||||
{showGoogleLogin && (
|
||||
<>
|
||||
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
|
||||
<div className="absolute text-xs bg-white px-3">Or</div>
|
||||
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
|
||||
<div className="absolute bg-white px-3 text-xs">Or</div>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-x-2">
|
||||
<a
|
||||
aria-label="Login with Google"
|
||||
className="flex w-full items-center justify-left space-x-3 rounded-md border border-gray-300 py-3 px-5 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1 hover:bg-gray-50"
|
||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
||||
href={`${SERVER_URL}/oauth/google`}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="google" className="w-5 h-5"><path fill="#fbbb00" d="M113.47 309.408 95.648 375.94l-65.139 1.378C11.042 341.211 0 299.9 0 256c0-42.451 10.324-82.483 28.624-117.732h.014L86.63 148.9l25.404 57.644c-5.317 15.501-8.215 32.141-8.215 49.456.002 18.792 3.406 36.797 9.651 53.408z"></path><path fill="#518ef8" d="M507.527 208.176C510.467 223.662 512 239.655 512 256c0 18.328-1.927 36.206-5.598 53.451-12.462 58.683-45.025 109.925-90.134 146.187l-.014-.014-73.044-3.727-10.338-64.535c29.932-17.554 53.324-45.025 65.646-77.911h-136.89V208.176h245.899z"></path><path fill="#28b446" d="m416.253 455.624.014.014C372.396 490.901 316.666 512 256 512c-97.491 0-182.252-54.491-225.491-134.681l82.961-67.91c21.619 57.698 77.278 98.771 142.53 98.771 28.047 0 54.323-7.582 76.87-20.818l83.383 68.262z"></path><path fill="#f14336" d="m419.404 58.936-82.933 67.896C313.136 112.246 285.552 103.82 256 103.82c-66.729 0-123.429 42.957-143.965 102.724l-83.397-68.276h-.014C71.23 56.123 157.06 0 256 0c62.115 0 119.068 22.126 163.404 58.936z"></path></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
id="google"
|
||||
className="h-5 w-5"
|
||||
>
|
||||
<path
|
||||
fill="#fbbb00"
|
||||
d="M113.47 309.408 95.648 375.94l-65.139 1.378C11.042 341.211 0 299.9 0 256c0-42.451 10.324-82.483 28.624-117.732h.014L86.63 148.9l25.404 57.644c-5.317 15.501-8.215 32.141-8.215 49.456.002 18.792 3.406 36.797 9.651 53.408z"
|
||||
></path>
|
||||
<path
|
||||
fill="#518ef8"
|
||||
d="M507.527 208.176C510.467 223.662 512 239.655 512 256c0 18.328-1.927 36.206-5.598 53.451-12.462 58.683-45.025 109.925-90.134 146.187l-.014-.014-73.044-3.727-10.338-64.535c29.932-17.554 53.324-45.025 65.646-77.911h-136.89V208.176h245.899z"
|
||||
></path>
|
||||
<path
|
||||
fill="#28b446"
|
||||
d="m416.253 455.624.014.014C372.396 490.901 316.666 512 256 512c-97.491 0-182.252-54.491-225.491-134.681l82.961-67.91c21.619 57.698 77.278 98.771 142.53 98.771 28.047 0 54.323-7.582 76.87-20.818l83.383 68.262z"
|
||||
></path>
|
||||
<path
|
||||
fill="#f14336"
|
||||
d="m419.404 58.936-82.933 67.896C313.136 112.246 285.552 103.82 256 103.82c-66.729 0-123.429 42.957-143.965 102.724l-83.397-68.276h-.014C71.23 56.123 157.06 0 256 0c62.115 0 119.068 22.126 163.404 58.936z"
|
||||
></path>
|
||||
</svg>
|
||||
<p>Login with Google</p>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -1,58 +1,54 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRegisterUserMutation, TRegisterUser } from "~/data-provider";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faFacebook } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRegisterUserMutation, TRegisterUser } from '~/data-provider';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faFacebook } from '@fortawesome/free-brands-svg-icons';
|
||||
import { faGoogle } from '@fortawesome/free-brands-svg-icons';
|
||||
|
||||
function Registration() {
|
||||
const SERVER_URL = import.meta.env.DEV
|
||||
? import.meta.env.VITE_SERVER_URL_DEV
|
||||
: import.meta.env.VITE_SERVER_URL_PROD;
|
||||
const showGoogleLogin =
|
||||
import.meta.env.VITE_SHOW_GOOGLE_LOGIN_OPTION === "true";
|
||||
const showGoogleLogin = import.meta.env.VITE_SHOW_GOOGLE_LOGIN_OPTION === 'true';
|
||||
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
register,
|
||||
watch,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<TRegisterUser>({ mode: "onChange" });
|
||||
formState: { errors }
|
||||
} = useForm<TRegisterUser>({ mode: 'onChange' });
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
const registerUser = useRegisterUserMutation();
|
||||
|
||||
const password = watch("password");
|
||||
const password = watch('password');
|
||||
|
||||
const onRegisterUserFormSubmit = (data: TRegisterUser) => {
|
||||
registerUser.mutate(data, {
|
||||
onSuccess: () => {
|
||||
navigate("/chat/new");
|
||||
navigate('/chat/new');
|
||||
},
|
||||
onError: (error) => {
|
||||
setError(true);
|
||||
if (error.response?.data?.message) {
|
||||
setErrorMessage(error.response?.data?.message);
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center pt-6 justify-center sm:pt-0 bg-white">
|
||||
<div className="mt-6 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg w-96">
|
||||
<h1 className="text-center text-3xl font-semibold mb-4">
|
||||
Create your account
|
||||
</h1>
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
||||
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
||||
<h1 className="mb-4 text-center text-3xl font-semibold">Create your account</h1>
|
||||
{error && (
|
||||
<div
|
||||
className="mt-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"
|
||||
className="relative mt-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"
|
||||
role="alert"
|
||||
>
|
||||
There was an error attempting to register your account. Please try
|
||||
again. {errorMessage}
|
||||
There was an error attempting to register your account. Please try again. {errorMessage}
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
@@ -73,29 +69,29 @@ function Registration() {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
{...register("name", {
|
||||
required: "Name is required",
|
||||
{...register('name', {
|
||||
required: 'Name is required',
|
||||
minLength: {
|
||||
value: 3,
|
||||
message: "Name must be at least 3 characters",
|
||||
message: 'Name must be at least 3 characters'
|
||||
},
|
||||
maxLength: {
|
||||
value: 80,
|
||||
message: "Name must be less than 80 characters",
|
||||
},
|
||||
message: 'Name must be less than 80 characters'
|
||||
}
|
||||
})}
|
||||
aria-invalid={!!errors.name}
|
||||
className="block rounded-t-md px-2.5 pb-2.5 pt-5 w-full text-sm text-gray-900 bg-gray-50 border-0 border-b-2 border-gray-300 appearance-none focus:outline-none focus:ring-0 focus:border-green-500 peer"
|
||||
className="peer block w-full appearance-none rounded-t-md border-0 border-b-2 border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
||||
placeholder=" "
|
||||
></input>
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="absolute text-sm text-gray-500 duration-300 transform -translate-y-4 scale-75 top-4 z-10 origin-[0] left-2.5 peer-focus:text-green-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-4"
|
||||
className="absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
||||
>
|
||||
Full Name
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
{errors.name && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-600">
|
||||
{/* @ts-ignore */}
|
||||
@@ -109,25 +105,25 @@ function Registration() {
|
||||
type="text"
|
||||
id="username"
|
||||
aria-label="Username"
|
||||
{...register("username", {
|
||||
required: "Username is required",
|
||||
{...register('username', {
|
||||
required: 'Username is required',
|
||||
minLength: {
|
||||
value: 3,
|
||||
message: "Username must be at least 3 characters",
|
||||
message: 'Username must be at least 3 characters'
|
||||
},
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: "Username must be less than 20 characters",
|
||||
},
|
||||
message: 'Username must be less than 20 characters'
|
||||
}
|
||||
})}
|
||||
aria-invalid={!!errors.username}
|
||||
className="block rounded-t-md px-2.5 pb-2.5 pt-5 w-full text-sm text-gray-900 bg-gray-50 border-0 border-b-2 border-gray-300 appearance-none focus:outline-none focus:ring-0 focus:border-green-500 peer"
|
||||
className="peer block w-full appearance-none rounded-t-md border-0 border-b-2 border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
||||
placeholder=" "
|
||||
autoComplete="off"
|
||||
></input>
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="absolute text-sm text-gray-500 duration-300 transform -translate-y-4 scale-75 top-4 z-10 origin-[0] left-2.5 peer-focus:text-green-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-4"
|
||||
className="absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
@@ -147,28 +143,28 @@ function Registration() {
|
||||
id="email"
|
||||
autoComplete="email"
|
||||
aria-label="Email"
|
||||
{...register("email", {
|
||||
required: "Email is required",
|
||||
{...register('email', {
|
||||
required: 'Email is required',
|
||||
minLength: {
|
||||
value: 3,
|
||||
message: "Email must be at least 6 characters",
|
||||
message: 'Email must be at least 6 characters'
|
||||
},
|
||||
maxLength: {
|
||||
value: 120,
|
||||
message: "Email should not be longer than 120 characters",
|
||||
message: 'Email should not be longer than 120 characters'
|
||||
},
|
||||
pattern: {
|
||||
value: /\S+@\S+\.\S+/,
|
||||
message: "You must enter a valid email address",
|
||||
},
|
||||
message: 'You must enter a valid email address'
|
||||
}
|
||||
})}
|
||||
aria-invalid={!!errors.email}
|
||||
className="block rounded-t-md px-2.5 pb-2.5 pt-5 w-full text-sm text-gray-900 bg-gray-50 border-0 border-b-2 border-gray-300 appearance-none focus:outline-none focus:ring-0 focus:border-green-500 peer"
|
||||
className="peer block w-full appearance-none rounded-t-md border-0 border-b-2 border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
||||
placeholder=" "
|
||||
></input>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="absolute text-sm text-gray-500 duration-300 transform -translate-y-4 scale-75 top-4 z-10 origin-[0] left-2.5 peer-focus:text-green-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-4"
|
||||
className="absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
@@ -187,24 +183,24 @@ function Registration() {
|
||||
id="password"
|
||||
autoComplete="current-password"
|
||||
aria-label="Password"
|
||||
{...register("password", {
|
||||
required: "Password is required",
|
||||
{...register('password', {
|
||||
required: 'Password is required',
|
||||
minLength: {
|
||||
value: 8,
|
||||
message: "Password must be at least 8 characters",
|
||||
message: 'Password must be at least 8 characters'
|
||||
},
|
||||
maxLength: {
|
||||
value: 40,
|
||||
message: "Password must be less than 40 characters",
|
||||
},
|
||||
message: 'Password must be less than 40 characters'
|
||||
}
|
||||
})}
|
||||
aria-invalid={!!errors.password}
|
||||
className="block rounded-t-md px-2.5 pb-2.5 pt-5 w-full text-sm text-gray-900 bg-gray-50 border-0 border-b-2 border-gray-300 appearance-none focus:outline-none focus:ring-0 focus:border-green-500 peer"
|
||||
className="peer block w-full appearance-none rounded-t-md border-0 border-b-2 border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
||||
placeholder=" "
|
||||
></input>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="absolute text-sm text-gray-500 duration-300 transform -translate-y-4 scale-75 top-4 z-10 origin-[0] left-2.5 peer-focus:text-green-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-4"
|
||||
className="absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
@@ -228,17 +224,16 @@ function Registration() {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
{...register("confirm_password", {
|
||||
validate: (value) =>
|
||||
value === password || "Passwords do not match",
|
||||
{...register('confirm_password', {
|
||||
validate: (value) => value === password || 'Passwords do not match'
|
||||
})}
|
||||
aria-invalid={!!errors.confirm_password}
|
||||
className="block rounded-t-md px-2.5 pb-2.5 pt-5 w-full text-sm text-gray-900 bg-gray-50 border-0 border-b-2 border-gray-300 appearance-none focus:outline-none focus:ring-0 focus:border-green-500 peer"
|
||||
className="peer block w-full appearance-none rounded-t-md border-0 border-b-2 border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
||||
placeholder=" "
|
||||
></input>
|
||||
<label
|
||||
htmlFor="confirm_password"
|
||||
className="absolute text-sm text-gray-500 duration-300 transform -translate-y-4 scale-75 top-4 z-10 origin-[0] left-2.5 peer-focus:text-green-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-4"
|
||||
className="absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
||||
>
|
||||
Confirm Password
|
||||
</label>
|
||||
@@ -268,29 +263,48 @@ function Registration() {
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<p className="my-4 text-center text-sm font-light text-gray-700">
|
||||
{" "}
|
||||
Already have an account?{" "}
|
||||
<a
|
||||
href="/login"
|
||||
className="font-medium text-green-500 p-1 hover:underline"
|
||||
>
|
||||
<p className="my-4 text-center text-sm font-light text-gray-700">
|
||||
{' '}
|
||||
Already have an account?{' '}
|
||||
<a href="/login" className="p-1 font-medium text-green-500 hover:underline">
|
||||
Login
|
||||
</a>
|
||||
</p>
|
||||
{showGoogleLogin && (
|
||||
<>
|
||||
<div className="relative mt-6 flex w-full items-center justify-center border border-t uppercase">
|
||||
<div className="absolute text-xs bg-white px-3">Or</div>
|
||||
<div className="absolute bg-white px-3 text-xs">Or</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex gap-x-2">
|
||||
<a
|
||||
aria-label="Login with Google"
|
||||
href={`${SERVER_URL}/oauth/google`}
|
||||
className="flex w-full items-center justify-left space-x-3 rounded-md border border-gray-300 py-3 px-5 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1 hover:bg-gray-50"
|
||||
className="justify-left flex w-full items-center space-x-3 rounded-md border border-gray-300 px-5 py-3 hover:bg-gray-50 focus:ring-2 focus:ring-violet-600 focus:ring-offset-1"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" id="google" className="w-5 h-5"><path fill="#fbbb00" d="M113.47 309.408 95.648 375.94l-65.139 1.378C11.042 341.211 0 299.9 0 256c0-42.451 10.324-82.483 28.624-117.732h.014L86.63 148.9l25.404 57.644c-5.317 15.501-8.215 32.141-8.215 49.456.002 18.792 3.406 36.797 9.651 53.408z"></path><path fill="#518ef8" d="M507.527 208.176C510.467 223.662 512 239.655 512 256c0 18.328-1.927 36.206-5.598 53.451-12.462 58.683-45.025 109.925-90.134 146.187l-.014-.014-73.044-3.727-10.338-64.535c29.932-17.554 53.324-45.025 65.646-77.911h-136.89V208.176h245.899z"></path><path fill="#28b446" d="m416.253 455.624.014.014C372.396 490.901 316.666 512 256 512c-97.491 0-182.252-54.491-225.491-134.681l82.961-67.91c21.619 57.698 77.278 98.771 142.53 98.771 28.047 0 54.323-7.582 76.87-20.818l83.383 68.262z"></path><path fill="#f14336" d="m419.404 58.936-82.933 67.896C313.136 112.246 285.552 103.82 256 103.82c-66.729 0-123.429 42.957-143.965 102.724l-83.397-68.276h-.014C71.23 56.123 157.06 0 256 0c62.115 0 119.068 22.126 163.404 58.936z"></path></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
id="google"
|
||||
className="h-5 w-5"
|
||||
>
|
||||
<path
|
||||
fill="#fbbb00"
|
||||
d="M113.47 309.408 95.648 375.94l-65.139 1.378C11.042 341.211 0 299.9 0 256c0-42.451 10.324-82.483 28.624-117.732h.014L86.63 148.9l25.404 57.644c-5.317 15.501-8.215 32.141-8.215 49.456.002 18.792 3.406 36.797 9.651 53.408z"
|
||||
></path>
|
||||
<path
|
||||
fill="#518ef8"
|
||||
d="M507.527 208.176C510.467 223.662 512 239.655 512 256c0 18.328-1.927 36.206-5.598 53.451-12.462 58.683-45.025 109.925-90.134 146.187l-.014-.014-73.044-3.727-10.338-64.535c29.932-17.554 53.324-45.025 65.646-77.911h-136.89V208.176h245.899z"
|
||||
></path>
|
||||
<path
|
||||
fill="#28b446"
|
||||
d="m416.253 455.624.014.014C372.396 490.901 316.666 512 256 512c-97.491 0-182.252-54.491-225.491-134.681l82.961-67.91c21.619 57.698 77.278 98.771 142.53 98.771 28.047 0 54.323-7.582 76.87-20.818l83.383 68.262z"
|
||||
></path>
|
||||
<path
|
||||
fill="#f14336"
|
||||
d="m419.404 58.936-82.933 67.896C313.136 112.246 285.552 103.82 256 103.82c-66.729 0-123.429 42.957-143.965 102.724l-83.397-68.276h-.014C71.23 56.123 157.06 0 256 0c62.115 0 119.068 22.126 163.404 58.936z"
|
||||
></path>
|
||||
</svg>
|
||||
<p>Login with Google</p>
|
||||
</a>
|
||||
{/* <button
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRequestPasswordResetMutation, TRequestPasswordReset } from "~/data-provider";
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRequestPasswordResetMutation, TRequestPasswordReset } from '~/data-provider';
|
||||
|
||||
function RequestPasswordReset() {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
formState: { errors }
|
||||
} = useForm<TRequestPasswordReset>();
|
||||
const requestPasswordReset = useRequestPasswordResetMutation();
|
||||
const [success, setSuccess] = useState<boolean>(false);
|
||||
const [requestError, setRequestError] = useState<boolean>(false);
|
||||
const [resetLink, setResetLink] = useState<string>("");
|
||||
const [resetLink, setResetLink] = useState<string>('');
|
||||
|
||||
const onSubmit = (data: TRequestPasswordReset) => {
|
||||
requestPasswordReset.mutate(data, {
|
||||
@@ -29,26 +29,29 @@ function RequestPasswordReset() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center pt-6 justify-center sm:pt-0 bg-white">
|
||||
<div className="mt-6 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg w-96">
|
||||
<h1 className="text-center text-3xl font-semibold mb-4">
|
||||
Reset your password
|
||||
</h1>
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
||||
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
||||
<h1 className="mb-4 text-center text-3xl font-semibold">Reset your password</h1>
|
||||
{success && (
|
||||
<div
|
||||
className="mt-4 bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative"
|
||||
className="relative mt-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-green-700"
|
||||
role="alert"
|
||||
>
|
||||
Click <a className="text-green-600 hover:underline" href={resetLink}>HERE</a> to reset your password.
|
||||
Click{' '}
|
||||
<a className="text-green-600 hover:underline" href={resetLink}>
|
||||
HERE
|
||||
</a>{' '}
|
||||
to reset your password.
|
||||
{/* An email has been sent with instructions on how to reset your password. */}
|
||||
</div>
|
||||
)}
|
||||
{requestError && (
|
||||
<div
|
||||
className="mt-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"
|
||||
className="relative mt-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"
|
||||
role="alert"
|
||||
>
|
||||
There was a problem resetting your password. There was no user found with the email address provided. Please try again.
|
||||
There was a problem resetting your password. There was no user found with the email
|
||||
address provided. Please try again.
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
@@ -59,33 +62,33 @@ function RequestPasswordReset() {
|
||||
>
|
||||
<div className="mb-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
autoComplete="off"
|
||||
aria-label="Email"
|
||||
{...register("email", {
|
||||
required: "Email is required",
|
||||
{...register('email', {
|
||||
required: 'Email is required',
|
||||
minLength: {
|
||||
value: 3,
|
||||
message: "Email must be at least 6 characters",
|
||||
message: 'Email must be at least 6 characters'
|
||||
},
|
||||
maxLength: {
|
||||
value: 120,
|
||||
message: "Email should not be longer than 120 characters",
|
||||
message: 'Email should not be longer than 120 characters'
|
||||
},
|
||||
pattern: {
|
||||
value: /\S+@\S+\.\S+/,
|
||||
message: "You must enter a valid email address",
|
||||
},
|
||||
message: 'You must enter a valid email address'
|
||||
}
|
||||
})}
|
||||
aria-invalid={!!errors.email}
|
||||
className="block rounded-t-md px-2.5 pb-2.5 pt-5 w-full text-sm text-gray-900 bg-gray-50 border-0 border-b-2 border-gray-300 appearance-none focus:outline-none focus:ring-0 focus:border-green-500 peer"
|
||||
className="peer block w-full appearance-none rounded-t-md border-0 border-b-2 border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
||||
placeholder=" "
|
||||
></input>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="absolute text-gray-500 duration-300 transform -translate-y-4 scale-75 top-4 z-10 origin-[0] left-2.5 peer-focus:text-green-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-4"
|
||||
className="absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-gray-500 duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
||||
>
|
||||
Email address
|
||||
</label>
|
||||
@@ -100,8 +103,8 @@ function RequestPasswordReset() {
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={ !!errors.email }
|
||||
className="w-full py-2 px-4 border border-transparent rounded-sm shadow-sm text-sm font-medium text-white bg-green-500 hover:bg-green-600 focus:outline-none active:bg-green-500"
|
||||
disabled={!!errors.email}
|
||||
className="w-full rounded-sm border border-transparent bg-green-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-green-600 focus:outline-none active:bg-green-500"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
@@ -112,4 +115,4 @@ function RequestPasswordReset() {
|
||||
);
|
||||
}
|
||||
|
||||
export default RequestPasswordReset;
|
||||
export default RequestPasswordReset;
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {useResetPasswordMutation, TResetPassword} from "~/data-provider";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useResetPasswordMutation, TResetPassword } from '~/data-provider';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
function ResetPassword() {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { errors },
|
||||
formState: { errors }
|
||||
} = useForm<TResetPassword>();
|
||||
const resetPassword = useResetPasswordMutation();
|
||||
const [resetError, setResetError] = useState<boolean>(false);
|
||||
const [params] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const password = watch("password");
|
||||
const password = watch('password');
|
||||
|
||||
const onSubmit = (data: TResetPassword) => {
|
||||
resetPassword.mutate(data, {
|
||||
@@ -26,19 +26,17 @@ function ResetPassword() {
|
||||
|
||||
if (resetPassword.isSuccess) {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center pt-6 justify-center sm:pt-0 bg-white">
|
||||
<div className="mt-6 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg w-96">
|
||||
<h1 className="text-center text-3xl font-semibold mb-4">
|
||||
Password Reset Success
|
||||
</h1>
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
||||
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
||||
<h1 className="mb-4 text-center text-3xl font-semibold">Password Reset Success</h1>
|
||||
<div
|
||||
className="mt-4 bg-green-100 border border-green-400 text-center mb-8 text-green-700 px-4 py-3 rounded relative"
|
||||
className="relative mb-8 mt-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-center text-green-700"
|
||||
role="alert"
|
||||
>
|
||||
You may now login with your new password.
|
||||
</div>
|
||||
<button
|
||||
onClick={() => navigate("/login")}
|
||||
onClick={() => navigate('/login')}
|
||||
aria-label="Sign in"
|
||||
className="w-full transform rounded-sm bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-600 focus:bg-green-600 focus:outline-none"
|
||||
>
|
||||
@@ -46,21 +44,22 @@ function ResetPassword() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
else {
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center pt-6 justify-center sm:pt-0 bg-white">
|
||||
<div className="mt-6 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg w-96">
|
||||
<h1 className="text-center text-3xl font-semibold mb-4">
|
||||
Reset your password
|
||||
</h1>
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-white pt-6 sm:pt-0">
|
||||
<div className="mt-6 w-96 overflow-hidden bg-white px-6 py-4 sm:max-w-md sm:rounded-lg">
|
||||
<h1 className="mb-4 text-center text-3xl font-semibold">Reset your password</h1>
|
||||
{resetError && (
|
||||
<div
|
||||
className="mt-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"
|
||||
className="relative mt-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"
|
||||
role="alert"
|
||||
>
|
||||
This password reset token is no longer valid. <a className="font-semibold hover:underline text-green-600" href="/forgot-password">Click here</a> to try again.
|
||||
This password reset token is no longer valid.{' '}
|
||||
<a className="font-semibold text-green-600 hover:underline" href="/forgot-password">
|
||||
Click here
|
||||
</a>{' '}
|
||||
to try again.
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
@@ -70,107 +69,113 @@ function ResetPassword() {
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="mb-2">
|
||||
<div className="relative">
|
||||
<input type="hidden" id="token" value={params.get("token")} {...register("token", { required: "Unable to process: No valid reset token" })} />
|
||||
<input type="hidden" id="userId" value={params.get("userId")} {...register("userId", { required: "Unable to process: No valid user id" })} />
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
autoComplete="current-password"
|
||||
aria-label="Password"
|
||||
{...register("password", {
|
||||
required: "Password is required",
|
||||
minLength: {
|
||||
value: 8,
|
||||
message: "Password must be at least 8 characters",
|
||||
},
|
||||
maxLength: {
|
||||
value: 40,
|
||||
message: "Password must be less than 40 characters",
|
||||
},
|
||||
})}
|
||||
aria-invalid={!!errors.password}
|
||||
className="block rounded-t-md px-2.5 pb-2.5 pt-5 w-full text-sm text-gray-900 bg-gray-50 border-0 border-b-2 border-gray-300 appearance-none focus:outline-none focus:ring-0 focus:border-green-500 peer"
|
||||
placeholder=" "
|
||||
></input>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="absolute text-sm text-gray-500 duration-300 transform -translate-y-4 scale-75 top-4 z-10 origin-[0] left-2.5 peer-focus:text-green-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-4"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="hidden"
|
||||
id="token"
|
||||
value={params.get('token')}
|
||||
{...register('token', { required: 'Unable to process: No valid reset token' })}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
id="userId"
|
||||
value={params.get('userId')}
|
||||
{...register('userId', { required: 'Unable to process: No valid user id' })}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
autoComplete="current-password"
|
||||
aria-label="Password"
|
||||
{...register('password', {
|
||||
required: 'Password is required',
|
||||
minLength: {
|
||||
value: 8,
|
||||
message: 'Password must be at least 8 characters'
|
||||
},
|
||||
maxLength: {
|
||||
value: 40,
|
||||
message: 'Password must be less than 40 characters'
|
||||
}
|
||||
})}
|
||||
aria-invalid={!!errors.password}
|
||||
className="peer block w-full appearance-none rounded-t-md border-0 border-b-2 border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
||||
placeholder=" "
|
||||
></input>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{errors.password && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-600">
|
||||
{/* @ts-ignore */}
|
||||
{errors.password.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="password"
|
||||
id="confirm_password"
|
||||
aria-label="Confirm Password"
|
||||
// uncomment to prevent pasting in confirm field
|
||||
onPaste={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
{...register("confirm_password", {
|
||||
validate: (value) =>
|
||||
value === password || "Passwords do not match",
|
||||
})}
|
||||
aria-invalid={!!errors.confirm_password}
|
||||
className="block rounded-t-md px-2.5 pb-2.5 pt-5 w-full text-sm text-gray-900 bg-gray-50 border-0 border-b-2 border-gray-300 appearance-none focus:outline-none focus:ring-0 focus:border-green-500 peer"
|
||||
placeholder=" "
|
||||
></input>
|
||||
<label
|
||||
htmlFor="confirm_password"
|
||||
className="absolute text-sm text-gray-500 duration-300 transform -translate-y-4 scale-75 top-4 z-10 origin-[0] left-2.5 peer-focus:text-green-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:translate-y-0 peer-focus:scale-75 peer-focus:-translate-y-4"
|
||||
>
|
||||
Confirm Password
|
||||
</label>
|
||||
{errors.password && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-600">
|
||||
{/* @ts-ignore */}
|
||||
{errors.password.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{errors.confirm_password && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-600">
|
||||
{/* @ts-ignore */}
|
||||
{errors.confirm_password.message}
|
||||
</span>
|
||||
)}
|
||||
{errors.token && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-600">
|
||||
{/* @ts-ignore */}
|
||||
{errors.token.message}
|
||||
</span>
|
||||
)}
|
||||
{errors.userId && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-600">
|
||||
{/* @ts-ignore */}
|
||||
{errors.userId.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
disabled={
|
||||
!!errors.password ||
|
||||
!!errors.confirm_password
|
||||
}
|
||||
type="submit"
|
||||
aria-label="Submit registration"
|
||||
className="w-full transform rounded-sm bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-600 focus:bg-green-600 focus:outline-none"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="mb-2">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="password"
|
||||
id="confirm_password"
|
||||
aria-label="Confirm Password"
|
||||
// uncomment to prevent pasting in confirm field
|
||||
onPaste={(e) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}}
|
||||
{...register('confirm_password', {
|
||||
validate: (value) => value === password || 'Passwords do not match'
|
||||
})}
|
||||
aria-invalid={!!errors.confirm_password}
|
||||
className="peer block w-full appearance-none rounded-t-md border-0 border-b-2 border-gray-300 bg-gray-50 px-2.5 pb-2.5 pt-5 text-sm text-gray-900 focus:border-green-500 focus:outline-none focus:ring-0"
|
||||
placeholder=" "
|
||||
></input>
|
||||
<label
|
||||
htmlFor="confirm_password"
|
||||
className="absolute left-2.5 top-4 z-10 origin-[0] -translate-y-4 scale-75 transform text-sm text-gray-500 duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:text-green-500"
|
||||
>
|
||||
Confirm Password
|
||||
</label>
|
||||
</div>
|
||||
{errors.confirm_password && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-600">
|
||||
{/* @ts-ignore */}
|
||||
{errors.confirm_password.message}
|
||||
</span>
|
||||
)}
|
||||
{errors.token && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-600">
|
||||
{/* @ts-ignore */}
|
||||
{errors.token.message}
|
||||
</span>
|
||||
)}
|
||||
{errors.userId && (
|
||||
<span role="alert" className="mt-1 text-sm text-red-600">
|
||||
{/* @ts-ignore */}
|
||||
{errors.userId.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
disabled={!!errors.password || !!errors.confirm_password}
|
||||
type="submit"
|
||||
aria-label="Submit registration"
|
||||
className="w-full transform rounded-sm bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-600 focus:bg-green-600 focus:outline-none"
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default ResetPassword;
|
||||
export default ResetPassword;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { default as Login } from './Login';
|
||||
export { default as Registration } from './Registration';
|
||||
export { default as RequestPasswordReset } from './RequestPasswordReset';
|
||||
export { default as ResetPassword } from './ResetPassword';
|
||||
export { default as ResetPassword } from './ResetPassword';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useEffect} from 'react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { useUpdateConversationMutation } from '~/data-provider';
|
||||
import RenameButton from './RenameButton';
|
||||
@@ -38,7 +38,7 @@ export default function Conversation({ conversation, retainView }) {
|
||||
switchToConversation(conversation);
|
||||
};
|
||||
|
||||
const renameHandler = e => {
|
||||
const renameHandler = (e) => {
|
||||
e.preventDefault();
|
||||
setTitleInput(title);
|
||||
setRenaming(true);
|
||||
@@ -47,12 +47,12 @@ export default function Conversation({ conversation, retainView }) {
|
||||
}, 25);
|
||||
};
|
||||
|
||||
const cancelHandler = e => {
|
||||
const cancelHandler = (e) => {
|
||||
e.preventDefault();
|
||||
setRenaming(false);
|
||||
};
|
||||
|
||||
const onRename = e => {
|
||||
const onRename = (e) => {
|
||||
e.preventDefault();
|
||||
setRenaming(false);
|
||||
if (titleInput === title) {
|
||||
@@ -61,11 +61,11 @@ export default function Conversation({ conversation, retainView }) {
|
||||
updateConvoMutation.mutate({ conversationId, title: titleInput });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (updateConvoMutation.isSuccess) {
|
||||
refreshConversations();
|
||||
if (conversationId == currentConversation?.conversationId) {
|
||||
setCurrentConversation(prevState => ({
|
||||
setCurrentConversation((prevState) => ({
|
||||
...prevState,
|
||||
title: titleInput
|
||||
}));
|
||||
@@ -73,7 +73,7 @@ export default function Conversation({ conversation, retainView }) {
|
||||
}
|
||||
}, [updateConvoMutation.isSuccess]);
|
||||
|
||||
const handleKeyDown = e => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
onRename(e);
|
||||
}
|
||||
@@ -90,10 +90,7 @@ export default function Conversation({ conversation, retainView }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
onClick={() => clickHandler()}
|
||||
{...aProps}
|
||||
>
|
||||
<a onClick={() => clickHandler()} {...aProps}>
|
||||
<ConvoIcon />
|
||||
<div className="relative max-h-5 flex-1 overflow-hidden text-ellipsis break-all">
|
||||
{renaming === true ? (
|
||||
@@ -102,7 +99,7 @@ export default function Conversation({ conversation, retainView }) {
|
||||
type="text"
|
||||
className="m-0 mr-0 w-full border border-blue-500 bg-transparent p-0 text-sm leading-tight outline-none"
|
||||
value={titleInput}
|
||||
onChange={e => setTitleInput(e.target.value)}
|
||||
onChange={(e) => setTitleInput(e.target.value)}
|
||||
onBlur={onRename}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
@@ -126,7 +123,7 @@ export default function Conversation({ conversation, retainView }) {
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="absolute inset-y-0 right-0 z-10 w-8 bg-gradient-to-l from-gray-900 group-hover:from-[#2A2B32]" />
|
||||
<div className="absolute inset-y-0 right-0 z-10 w-8 rounded-r-md bg-gradient-to-l from-gray-900 group-hover:from-[#2A2B32]" />
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -14,26 +14,22 @@ export default function DeleteButton({ conversationId, renaming, cancelHandler,
|
||||
const deleteConvoMutation = useDeleteConversationMutation(conversationId);
|
||||
|
||||
useEffect(() => {
|
||||
if(deleteConvoMutation.isSuccess) {
|
||||
if (currentConversation?.conversationId == conversationId) newConversation();
|
||||
|
||||
if (deleteConvoMutation.isSuccess) {
|
||||
if (currentConversation?.conversationId == conversationId) newConversation();
|
||||
|
||||
refreshConversations();
|
||||
retainView();
|
||||
}
|
||||
}, [deleteConvoMutation.isSuccess]);
|
||||
|
||||
|
||||
const clickHandler = () => {
|
||||
deleteConvoMutation.mutate({conversationId, source: 'button' });
|
||||
deleteConvoMutation.mutate({ conversationId, source: 'button' });
|
||||
};
|
||||
|
||||
const handler = renaming ? cancelHandler : clickHandler;
|
||||
|
||||
return (
|
||||
<button
|
||||
className="p-1 hover:text-white"
|
||||
onClick={handler}
|
||||
>
|
||||
<button className="p-1 hover:text-white" onClick={handler}>
|
||||
{renaming ? <CrossIcon /> : <TrashIcon />}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Pages({ pageNumber, pages, nextPage, previousPage }) {
|
||||
const clickHandler = func => async e => {
|
||||
const clickHandler = (func) => async (e) => {
|
||||
e.preventDefault();
|
||||
await func();
|
||||
};
|
||||
|
||||
return pageNumber == 1 && pages == 1 ? null : (
|
||||
<div className="m-auto mt-4 mb-2 flex items-center justify-center gap-2">
|
||||
<div className="m-auto mb-2 mt-4 flex items-center justify-center gap-2">
|
||||
<button
|
||||
onClick={clickHandler(previousPage)}
|
||||
className={
|
||||
|
||||
@@ -4,7 +4,7 @@ import CheckMark from '../svg/CheckMark';
|
||||
|
||||
export default function RenameButton({ renaming, renameHandler, onRename, twcss }) {
|
||||
const handler = renaming ? onRename : renameHandler;
|
||||
const classProp = { className: "p-1 hover:text-white" };
|
||||
const classProp = { className: 'p-1 hover:text-white' };
|
||||
if (twcss) {
|
||||
classProp.className = twcss;
|
||||
}
|
||||
|
||||
@@ -6,13 +6,9 @@ export default function Conversations({ conversations, conversationId, moveToTop
|
||||
<>
|
||||
{conversations &&
|
||||
conversations.length > 0 &&
|
||||
conversations.map(convo => {
|
||||
conversations.map((convo) => {
|
||||
return (
|
||||
<Conversation
|
||||
key={convo.conversationId}
|
||||
conversation={convo}
|
||||
retainView={moveToTop}
|
||||
/>
|
||||
<Conversation key={convo.conversationId} conversation={convo} retainView={moveToTop} />
|
||||
);
|
||||
})}
|
||||
</>
|
||||
|
||||
@@ -17,37 +17,36 @@ function Settings(props) {
|
||||
const setContext = setOption('context');
|
||||
const setSystemMessage = setOption('systemMessage');
|
||||
const setJailbreak = setOption('jailbreak');
|
||||
const setToneStyle = value => setOption('toneStyle')(value.toLowerCase());
|
||||
const setToneStyle = (value) => setOption('toneStyle')(value.toLowerCase());
|
||||
const debouncedContext = useDebounce(context, 250);
|
||||
const updateTokenCountMutation = useUpdateTokenCountMutation();
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!debouncedContext || debouncedContext.trim() === '') {
|
||||
setTokenCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const handleTextChange = context => {
|
||||
updateTokenCountMutation.mutate({ text: context }, {
|
||||
onSuccess: data => {
|
||||
setTokenCount(data.count);
|
||||
const handleTextChange = (context) => {
|
||||
updateTokenCountMutation.mutate(
|
||||
{ text: context },
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
setTokenCount(data.count);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
handleTextChange(debouncedContext);
|
||||
}, [debouncedContext]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="max-h-[350px] overflow-y-auto">
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="toneStyle-dropdown"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
<Label htmlFor="toneStyle-dropdown" className="text-left text-sm font-medium">
|
||||
Tone Style <small className="opacity-40">(default: fast)</small>
|
||||
</Label>
|
||||
<SelectDropDown
|
||||
@@ -65,17 +64,14 @@ function Settings(props) {
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="context"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
<Label htmlFor="context" className="text-left text-sm font-medium">
|
||||
Context <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id="context"
|
||||
disabled={readonly}
|
||||
value={context || ''}
|
||||
onChange={e => setContext(e.target.value || null)}
|
||||
onChange={(e) => setContext(e.target.value || null)}
|
||||
placeholder="Bing can use up to 7k tokens for 'context', which it can reference for the conversation. The specific limit is not known but may run into errors exceeding 7k tokens"
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
@@ -87,10 +83,7 @@ function Settings(props) {
|
||||
</div>
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="jailbreak"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
<Label htmlFor="jailbreak" className="text-left text-sm font-medium">
|
||||
Enable Sydney <small className="opacity-40">(default: false)</small>
|
||||
</Label>
|
||||
<div className="flex h-[40px] w-full items-center space-x-3">
|
||||
@@ -130,7 +123,7 @@ function Settings(props) {
|
||||
id="systemMessage"
|
||||
disabled={readonly}
|
||||
value={systemMessage || ''}
|
||||
onChange={e => setSystemMessage(e.target.value || null)}
|
||||
onChange={(e) => setSystemMessage(e.target.value || null)}
|
||||
placeholder="WARNING: Misuse of this feature can get you BANNED from using Bing! Click on 'System Message' for full instructions and the default message if omitted, which is the 'Sydney' preset that is considered safe."
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
@@ -141,7 +134,7 @@ function Settings(props) {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Examples from './Google/Examples.jsx';
|
||||
import MessagesSquared from '~/components/svg/MessagesSquared.jsx';
|
||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import filenamify from 'filenamify';
|
||||
import axios from 'axios';
|
||||
@@ -7,6 +9,7 @@ import DialogTemplate from '../ui/DialogTemplate';
|
||||
import { Dialog, DialogClose, DialogButton } from '../ui/Dialog.tsx';
|
||||
import { Input } from '../ui/Input.tsx';
|
||||
import { Label } from '../ui/Label.tsx';
|
||||
import { Button } from '../ui/Button.tsx';
|
||||
import Dropdown from '../ui/Dropdown';
|
||||
import { cn } from '~/utils/';
|
||||
import cleanupPreset from '~/utils/cleanupPreset';
|
||||
@@ -18,15 +21,81 @@ import store from '~/store';
|
||||
const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
|
||||
// const [title, setTitle] = useState('My Preset');
|
||||
const [preset, setPreset] = useState(_preset);
|
||||
const [showExamples, setShowExamples] = useState(false);
|
||||
const setPresets = useSetRecoilState(store.presets);
|
||||
|
||||
const availableEndpoints = useRecoilValue(store.availableEndpoints);
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
|
||||
const setOption = param => newValue => {
|
||||
const triggerExamples = () => setShowExamples((prev) => !prev);
|
||||
|
||||
const setOption = (param) => (newValue) => {
|
||||
let update = {};
|
||||
update[param] = newValue;
|
||||
setPreset(prevState =>
|
||||
setPreset((prevState) =>
|
||||
cleanupPreset({
|
||||
preset: {
|
||||
...prevState,
|
||||
...update
|
||||
},
|
||||
endpointsConfig
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const setExample = (i, type, newValue = null) => {
|
||||
let update = {};
|
||||
let current = preset?.examples.slice() || [];
|
||||
let currentExample = { ...current[i] } || {};
|
||||
currentExample[type] = { content: newValue };
|
||||
current[i] = currentExample;
|
||||
update.examples = current;
|
||||
setPreset((prevState) =>
|
||||
cleanupPreset({
|
||||
preset: {
|
||||
...prevState,
|
||||
...update
|
||||
},
|
||||
endpointsConfig
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const addExample = () => {
|
||||
let update = {};
|
||||
let current = preset?.examples.slice() || [];
|
||||
current.push({ input: { content: '' }, output: { content: '' } });
|
||||
update.examples = current;
|
||||
setPreset((prevState) =>
|
||||
cleanupPreset({
|
||||
preset: {
|
||||
...prevState,
|
||||
...update
|
||||
},
|
||||
endpointsConfig
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const removeExample = () => {
|
||||
let update = {};
|
||||
let current = preset?.examples.slice() || [];
|
||||
if (current.length <= 1) {
|
||||
update.examples = [{ input: { content: '' }, output: { content: '' } }];
|
||||
setPreset((prevState) =>
|
||||
cleanupPreset({
|
||||
preset: {
|
||||
...prevState,
|
||||
...update
|
||||
},
|
||||
endpointsConfig
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
current.pop();
|
||||
update.examples = current;
|
||||
setPreset((prevState) =>
|
||||
cleanupPreset({
|
||||
preset: {
|
||||
...prevState,
|
||||
@@ -46,7 +115,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
|
||||
url: '/api/presets',
|
||||
data: cleanupPreset({ preset, endpointsConfig }),
|
||||
withCredentials: true
|
||||
}).then(res => {
|
||||
}).then((res) => {
|
||||
setPresets(res?.data);
|
||||
});
|
||||
};
|
||||
@@ -62,13 +131,11 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
|
||||
|
||||
useEffect(() => {
|
||||
setPreset(_preset);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTemplate
|
||||
title={`${title || 'Edit Preset'} - ${preset?.title}`}
|
||||
className="max-w-full sm:max-w-4xl"
|
||||
@@ -76,16 +143,13 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
<div className="grid w-full gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
|
||||
<Label
|
||||
htmlFor="chatGptLabel"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
<Label htmlFor="chatGptLabel" className="text-left text-sm font-medium">
|
||||
Preset Name
|
||||
</Label>
|
||||
<Input
|
||||
id="chatGptLabel"
|
||||
value={preset?.title || ''}
|
||||
onChange={e => setOption('title')(e.target.value || '')}
|
||||
onChange={(e) => setOption('title')(e.target.value || '')}
|
||||
placeholder="Set a custom name, in case you can find this preset"
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
@@ -94,10 +158,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
|
||||
<Label
|
||||
htmlFor="endpoint"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
<Label htmlFor="endpoint" className="text-left text-sm font-medium">
|
||||
Endpoint
|
||||
</Label>
|
||||
<Dropdown
|
||||
@@ -111,14 +172,33 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
|
||||
)}
|
||||
containerClassName="flex w-full resize-none"
|
||||
/>
|
||||
{preset?.endpoint === 'google' && (
|
||||
<Button
|
||||
type="button"
|
||||
className="ml-1 flex h-auto w-full bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-0 focus:ring-offset-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0"
|
||||
onClick={triggerExamples}
|
||||
>
|
||||
<MessagesSquared className="mr-1 w-[14px]" />
|
||||
{(showExamples ? 'Hide' : 'Show') + ' Examples'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-4 w-full border-t border-gray-300 dark:border-gray-500" />
|
||||
<div className="w-full p-0">
|
||||
<Settings
|
||||
preset={preset}
|
||||
setOption={setOption}
|
||||
/>
|
||||
{((preset?.endpoint === 'google' && !showExamples) ||
|
||||
preset?.endpoint !== 'google') && (
|
||||
<Settings preset={preset} setOption={setOption} />
|
||||
)}
|
||||
{preset?.endpoint === 'google' && showExamples && (
|
||||
<Examples
|
||||
examples={preset.examples}
|
||||
setExample={setExample}
|
||||
addExample={addExample}
|
||||
removeExample={removeExample}
|
||||
edit={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -134,10 +214,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
|
||||
}
|
||||
leftButtons={
|
||||
<>
|
||||
<DialogButton
|
||||
onClick={exportPreset}
|
||||
className="dark:hover:gray-400 border-gray-700"
|
||||
>
|
||||
<DialogButton onClick={exportPreset} className="dark:hover:gray-400 border-gray-700">
|
||||
Export
|
||||
</DialogButton>
|
||||
</>
|
||||
|
||||
@@ -12,16 +12,20 @@ import store from '~/store';
|
||||
|
||||
// A preset dialog to show readonly preset values.
|
||||
const EndpointOptionsDialog = ({ open, onOpenChange, preset: _preset, title }) => {
|
||||
// const [title, setTitle] = useState('My Preset');
|
||||
const [preset, setPreset] = useState(_preset);
|
||||
const [endpointName, setEndpointName] = useState(preset?.endpoint);
|
||||
|
||||
const [saveAsDialogShow, setSaveAsDialogShow] = useState(false);
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
|
||||
const setOption = param => newValue => {
|
||||
if (endpointName === 'google') {
|
||||
setEndpointName('PaLM');
|
||||
}
|
||||
|
||||
const setOption = (param) => (newValue) => {
|
||||
let update = {};
|
||||
update[param] = newValue;
|
||||
setPreset(prevState => ({
|
||||
setPreset((prevState) => ({
|
||||
...prevState,
|
||||
...update
|
||||
}));
|
||||
@@ -45,21 +49,14 @@ const EndpointOptionsDialog = ({ open, onOpenChange, preset: _preset, title }) =
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTemplate
|
||||
title={`${title || 'View Options'} - ${preset?.endpoint}`}
|
||||
title={`${title || 'View Options'} - ${endpointName}`}
|
||||
className="max-w-full sm:max-w-4xl"
|
||||
main={
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
<div className="w-full p-0">
|
||||
<Settings
|
||||
preset={preset}
|
||||
readonly={true}
|
||||
setOption={setOption}
|
||||
/>
|
||||
<Settings preset={preset} readonly={true} setOption={setOption} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -75,10 +72,7 @@ const EndpointOptionsDialog = ({ open, onOpenChange, preset: _preset, title }) =
|
||||
}
|
||||
leftButtons={
|
||||
<>
|
||||
<DialogButton
|
||||
onClick={exportPreset}
|
||||
className="dark:hover:gray-400 border-gray-700"
|
||||
>
|
||||
<DialogButton onClick={exportPreset} className="dark:hover:gray-400 border-gray-700">
|
||||
Export
|
||||
</DialogButton>
|
||||
</>
|
||||
|
||||
@@ -4,7 +4,13 @@ import CrossIcon from '../svg/CrossIcon';
|
||||
// import SaveIcon from '../svg/SaveIcon';
|
||||
import { Save } from 'lucide-react';
|
||||
|
||||
function EndpointOptionsPopover({ content, visible, saveAsPreset, switchToSimpleMode }) {
|
||||
function EndpointOptionsPopover({
|
||||
content,
|
||||
visible,
|
||||
saveAsPreset,
|
||||
switchToSimpleMode,
|
||||
additionalButton = null
|
||||
}) {
|
||||
const cardStyle =
|
||||
'shadow-md rounded-md min-w-[75px] font-normal bg-white border-black/10 border dark:bg-gray-700 text-black dark:text-white';
|
||||
|
||||
@@ -12,29 +18,39 @@ function EndpointOptionsPopover({ content, visible, saveAsPreset, switchToSimple
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
' endpointOptionsPopover-container absolute bottom-[-10px] flex w-full flex-col items-center justify-center md:px-4' +
|
||||
' endpointOptionsPopover-container absolute bottom-[-10px] flex w-full flex-col items-center md:px-4' +
|
||||
(visible ? ' show' : '')
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
cardStyle +
|
||||
' border-s-0 border-d-0 flex w-full flex-col overflow-hidden rounded-none border-t bg-slate-200 px-0 pb-[10px] dark:border-white/10 md:rounded-md md:border lg:w-[736px]'
|
||||
' border-d-0 flex w-full flex-col overflow-hidden rounded-none border-s-0 border-t bg-slate-200 px-0 pb-[10px] dark:border-white/10 md:rounded-md md:border lg:w-[736px]'
|
||||
}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between bg-slate-100 px-2 py-2 dark:bg-gray-800/60">
|
||||
<div className="flex w-full items-center bg-slate-100 px-2 py-2 dark:bg-gray-800/60">
|
||||
{/* <span className="text-xs font-medium font-normal">Advanced settings for OpenAI endpoint</span> */}
|
||||
<Button
|
||||
type="button"
|
||||
className="h-auto bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
className="h-auto justify-start bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0"
|
||||
onClick={saveAsPreset}
|
||||
>
|
||||
<Save className="mr-1 w-[14px]" />
|
||||
Save as preset
|
||||
</Button>
|
||||
{additionalButton && (
|
||||
<Button
|
||||
type="button"
|
||||
className="ml-1 h-auto justify-start bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-0 focus:ring-offset-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0"
|
||||
onClick={additionalButton.handler}
|
||||
>
|
||||
{additionalButton.icon}
|
||||
{additionalButton.label}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
className="h-auto bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
className="ml-auto h-auto bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-offset-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
onClick={switchToSimpleMode}
|
||||
>
|
||||
<CrossIcon className="mr-1" />
|
||||
|
||||
89
client/src/components/Endpoints/Google/Examples.jsx
Normal file
89
client/src/components/Endpoints/Google/Examples.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { Button } from '~/components/ui/Button.tsx';
|
||||
import { Label } from '~/components/ui/Label.tsx';
|
||||
import { Plus, Minus } from 'lucide-react';
|
||||
import { cn } from '~/utils/';
|
||||
const defaultTextProps =
|
||||
'rounded-md border border-gray-200 focus:border-slate-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 focus:dark:bg-gray-600 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0';
|
||||
|
||||
function Examples({ readonly, examples, setExample, addExample, removeExample, edit = false }) {
|
||||
const maxHeight = edit ? 'max-h-[233px]' : 'max-h-[350px]';
|
||||
return (
|
||||
<>
|
||||
<div className={`${maxHeight} overflow-y-auto`}>
|
||||
<div id="examples-grid" className="grid gap-6 sm:grid-cols-2">
|
||||
{examples.map((example, idx) => (
|
||||
<React.Fragment key={idx}>
|
||||
{/* Input */}
|
||||
<div
|
||||
className={`col-span-${
|
||||
examples.length === 1 ? '1' : 'full'
|
||||
} flex flex-col items-center justify-start gap-6 sm:col-span-1`}
|
||||
>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor={`input-${idx}`} className="text-left text-sm font-medium">
|
||||
Input <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id={`input-${idx}`}
|
||||
disabled={readonly}
|
||||
value={example?.input?.content || ''}
|
||||
onChange={(e) => setExample(idx, 'input', e.target.value || null)}
|
||||
placeholder="Set example input. Example is ignored if empty."
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex max-h-[300px] min-h-[75px] w-full resize-none px-3 py-2 '
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Output */}
|
||||
<div
|
||||
className={`col-span-${
|
||||
examples.length === 1 ? '1' : 'full'
|
||||
} flex flex-col items-center justify-start gap-6 sm:col-span-1`}
|
||||
>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor={`output-${idx}`} className="text-left text-sm font-medium">
|
||||
Output <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id={`output-${idx}`}
|
||||
disabled={readonly}
|
||||
value={example?.output?.content || ''}
|
||||
onChange={(e) => setExample(idx, 'output', e.target.value || null)}
|
||||
placeholder={`Set example output. Example is ignored if empty.`}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex max-h-[300px] min-h-[75px] w-full resize-none px-3 py-2 '
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
className="mr-2 mt-1 h-auto items-center justify-center bg-transparent px-3 py-2 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-0 focus:ring-offset-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0"
|
||||
onClick={removeExample}
|
||||
>
|
||||
<Minus className="w-[16px]" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-1 h-auto items-center justify-center bg-transparent px-3 py-2 text-xs font-medium font-normal text-black hover:bg-slate-200 hover:text-black focus:ring-0 focus:ring-offset-0 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-white dark:focus:outline-none dark:focus:ring-offset-0"
|
||||
onClick={addExample}
|
||||
>
|
||||
<Plus className="w-[16px]" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Examples;
|
||||
33
client/src/components/Endpoints/Google/OptionHover.jsx
Normal file
33
client/src/components/Endpoints/Google/OptionHover.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { HoverCardPortal, HoverCardContent } from '~/components/ui/HoverCard.tsx';
|
||||
|
||||
const types = {
|
||||
temp: 'Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.',
|
||||
topp: 'Top-p changes how the model selects tokens for output. Tokens are selected from most K (see topK parameter) probable to least until the sum of their probabilities equals the top-p value.',
|
||||
topk: "Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens in the model's vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature).",
|
||||
maxoutputtokens:
|
||||
' Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses.'
|
||||
};
|
||||
|
||||
function OptionHover({ type, side }) {
|
||||
// const options = {};
|
||||
// if (type === 'pres') {
|
||||
// options.sideOffset = 45;
|
||||
// }
|
||||
|
||||
return (
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent
|
||||
side={side}
|
||||
className="w-80 "
|
||||
// {...options}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">{types[type]}</p>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
);
|
||||
}
|
||||
|
||||
export default OptionHover;
|
||||
253
client/src/components/Endpoints/Google/Settings.jsx
Normal file
253
client/src/components/Endpoints/Google/Settings.jsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import SelectDropDown from '../../ui/SelectDropDown';
|
||||
import { Input } from '~/components/ui/Input.tsx';
|
||||
import { Label } from '~/components/ui/Label.tsx';
|
||||
import { Slider } from '~/components/ui/Slider.tsx';
|
||||
import { InputNumber } from '~/components/ui/InputNumber.tsx';
|
||||
import OptionHover from './OptionHover';
|
||||
import { HoverCard, HoverCardTrigger } from '~/components/ui/HoverCard.tsx';
|
||||
import { cn } from '~/utils/';
|
||||
const defaultTextProps =
|
||||
'rounded-md border border-gray-200 focus:border-slate-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 focus:dark:bg-gray-600 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0';
|
||||
|
||||
const optionText =
|
||||
'p-0 shadow-none text-right pr-1 h-8 border-transparent focus:ring-[#10a37f] focus:ring-offset-0 focus:ring-opacity-100 hover:bg-gray-800/10 dark:hover:bg-white/10 focus:bg-gray-800/10 dark:focus:bg-white/10 transition-colors';
|
||||
|
||||
import store from '~/store';
|
||||
|
||||
function Settings(props) {
|
||||
const {
|
||||
readonly,
|
||||
model,
|
||||
modelLabel,
|
||||
promptPrefix,
|
||||
temperature,
|
||||
topP,
|
||||
topK,
|
||||
maxOutputTokens,
|
||||
setOption,
|
||||
edit = false
|
||||
} = props;
|
||||
const maxHeight = edit ? 'max-h-[305px]' : 'max-h-[350px]';
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
|
||||
const setModel = setOption('model');
|
||||
const setModelLabel = setOption('modelLabel');
|
||||
const setPromptPrefix = setOption('promptPrefix');
|
||||
const setTemperature = setOption('temperature');
|
||||
const setTopP = setOption('topP');
|
||||
const setTopK = setOption('topK');
|
||||
const setMaxOutputTokens = setOption('maxOutputTokens');
|
||||
|
||||
const models = endpointsConfig?.['google']?.['availableModels'] || [];
|
||||
|
||||
return (
|
||||
<div className={`${maxHeight} overflow-y-auto`}>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<SelectDropDown
|
||||
value={model}
|
||||
setValue={setModel}
|
||||
availableValues={models}
|
||||
disabled={readonly}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex w-full resize-none focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0'
|
||||
)}
|
||||
containerClassName="flex w-full resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="modelLabel" className="text-left text-sm font-medium">
|
||||
Custom Name <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<Input
|
||||
id="modelLabel"
|
||||
disabled={readonly}
|
||||
value={modelLabel || ''}
|
||||
onChange={(e) => setModelLabel(e.target.value || null)}
|
||||
placeholder="Set a custom name for PaLM2"
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex h-10 max-h-10 w-full resize-none px-3 py-2 focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="promptPrefix" className="text-left text-sm font-medium">
|
||||
Prompt Prefix <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id="promptPrefix"
|
||||
disabled={readonly}
|
||||
value={promptPrefix || ''}
|
||||
onChange={(e) => setPromptPrefix(e.target.value || null)}
|
||||
placeholder="Set custom instructions or context. Ignored if empty."
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'flex max-h-[300px] min-h-[100px] w-full resize-none px-3 py-2 '
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
|
||||
Temperature <small className="opacity-40">(default: 0.2)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="temp-int"
|
||||
disabled={readonly}
|
||||
value={temperature}
|
||||
onChange={(value) => setTemperature(value)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[temperature]}
|
||||
onValueChange={(value) => setTemperature(value[0])}
|
||||
doubleClickHandler={() => setTemperature(1)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="temp" side="left" />
|
||||
</HoverCard>
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
|
||||
Top P <small className="opacity-40">(default: 0.95)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="top-p-int"
|
||||
disabled={readonly}
|
||||
value={topP}
|
||||
onChange={(value) => setTopP(value)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[topP]}
|
||||
onValueChange={(value) => setTopP(value[0])}
|
||||
doubleClickHandler={() => setTopP(1)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="topp" side="left" />
|
||||
</HoverCard>
|
||||
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="top-k-int" className="text-left text-sm font-medium">
|
||||
Top K <small className="opacity-40">(default: 40)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="top-k-int"
|
||||
disabled={readonly}
|
||||
value={topK}
|
||||
onChange={(value) => setTopK(value)}
|
||||
max={40}
|
||||
min={1}
|
||||
step={0.01}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[topK]}
|
||||
onValueChange={(value) => setTopK(value[0])}
|
||||
doubleClickHandler={() => setTopK(0)}
|
||||
max={40}
|
||||
min={1}
|
||||
step={0.01}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="topk" side="left" />
|
||||
</HoverCard>
|
||||
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label htmlFor="max-tokens-int" className="text-left text-sm font-medium">
|
||||
Max Output Tokens <small className="opacity-40">(default: 1024)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="max-tokens-int"
|
||||
disabled={readonly}
|
||||
value={maxOutputTokens}
|
||||
onChange={(value) => setMaxOutputTokens(value)}
|
||||
max={1024}
|
||||
min={1}
|
||||
step={1}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200'
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[maxOutputTokens]}
|
||||
onValueChange={(value) => setMaxOutputTokens(value[0])}
|
||||
doubleClickHandler={() => setMaxOutputTokens(0)}
|
||||
max={1024}
|
||||
min={1}
|
||||
step={1}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover type="maxoutputtokens" side="left" />
|
||||
</HoverCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
@@ -17,7 +17,17 @@ const optionText =
|
||||
import store from '~/store';
|
||||
|
||||
function Settings(props) {
|
||||
const { readonly, model, chatGptLabel, promptPrefix, temperature, topP, freqP, presP, setOption } = props;
|
||||
const {
|
||||
readonly,
|
||||
model,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
temperature,
|
||||
topP,
|
||||
freqP,
|
||||
presP,
|
||||
setOption
|
||||
} = props;
|
||||
|
||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||
|
||||
@@ -32,7 +42,7 @@ function Settings(props) {
|
||||
const models = endpointsConfig?.['openAI']?.['availableModels'] || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="max-h-[350px] overflow-y-auto">
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
@@ -49,17 +59,14 @@ function Settings(props) {
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="chatGptLabel"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
<Label htmlFor="chatGptLabel" className="text-left text-sm font-medium">
|
||||
Custom Name <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<Input
|
||||
id="chatGptLabel"
|
||||
disabled={readonly}
|
||||
value={chatGptLabel || ''}
|
||||
onChange={e => setChatGptLabel(e.target.value || null)}
|
||||
onChange={(e) => setChatGptLabel(e.target.value || null)}
|
||||
placeholder="Set a custom name for ChatGPT"
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
@@ -68,17 +75,14 @@ function Settings(props) {
|
||||
/>
|
||||
</div>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="promptPrefix"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
<Label htmlFor="promptPrefix" className="text-left text-sm font-medium">
|
||||
Prompt Prefix <small className="opacity-40">(default: blank)</small>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id="promptPrefix"
|
||||
disabled={readonly}
|
||||
value={promptPrefix || ''}
|
||||
onChange={e => setPromptPrefix(e.target.value || null)}
|
||||
onChange={(e) => setPromptPrefix(e.target.value || null)}
|
||||
placeholder="Set custom instructions. Defaults to: 'You are ChatGPT, a large language model trained by OpenAI.'"
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
@@ -91,17 +95,14 @@ function Settings(props) {
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label
|
||||
htmlFor="temp-int"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
|
||||
Temperature <small className="opacity-40">(default: 1)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="temp-int"
|
||||
disabled={readonly}
|
||||
value={temperature}
|
||||
onChange={value => setTemperature(value)}
|
||||
onChange={(value) => setTemperature(value)}
|
||||
max={2}
|
||||
min={0}
|
||||
step={0.01}
|
||||
@@ -118,7 +119,7 @@ function Settings(props) {
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[temperature]}
|
||||
onValueChange={value => setTemperature(value[0])}
|
||||
onValueChange={(value) => setTemperature(value[0])}
|
||||
doubleClickHandler={() => setTemperature(1)}
|
||||
max={2}
|
||||
min={0}
|
||||
@@ -126,25 +127,19 @@ function Settings(props) {
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover
|
||||
type="temp"
|
||||
side="left"
|
||||
/>
|
||||
<OptionHover type="temp" side="left" />
|
||||
</HoverCard>
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label
|
||||
htmlFor="top-p-int"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
|
||||
Top P <small className="opacity-40">(default: 1)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="top-p-int"
|
||||
disabled={readonly}
|
||||
value={topP}
|
||||
onChange={value => setTopP(value)}
|
||||
onChange={(value) => setTopP(value)}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
@@ -161,7 +156,7 @@ function Settings(props) {
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[topP]}
|
||||
onValueChange={value => setTopP(value[0])}
|
||||
onValueChange={(value) => setTopP(value[0])}
|
||||
doubleClickHandler={() => setTopP(1)}
|
||||
max={1}
|
||||
min={0}
|
||||
@@ -169,26 +164,20 @@ function Settings(props) {
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover
|
||||
type="topp"
|
||||
side="left"
|
||||
/>
|
||||
<OptionHover type="topp" side="left" />
|
||||
</HoverCard>
|
||||
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label
|
||||
htmlFor="freq-penalty-int"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
<Label htmlFor="freq-penalty-int" className="text-left text-sm font-medium">
|
||||
Frequency Penalty <small className="opacity-40">(default: 0)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="freq-penalty-int"
|
||||
disabled={readonly}
|
||||
value={freqP}
|
||||
onChange={value => setFreqP(value)}
|
||||
onChange={(value) => setFreqP(value)}
|
||||
max={2}
|
||||
min={-2}
|
||||
step={0.01}
|
||||
@@ -205,7 +194,7 @@ function Settings(props) {
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[freqP]}
|
||||
onValueChange={value => setFreqP(value[0])}
|
||||
onValueChange={(value) => setFreqP(value[0])}
|
||||
doubleClickHandler={() => setFreqP(0)}
|
||||
max={2}
|
||||
min={-2}
|
||||
@@ -213,26 +202,20 @@ function Settings(props) {
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover
|
||||
type="freq"
|
||||
side="left"
|
||||
/>
|
||||
<OptionHover type="freq" side="left" />
|
||||
</HoverCard>
|
||||
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label
|
||||
htmlFor="pres-penalty-int"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
<Label htmlFor="pres-penalty-int" className="text-left text-sm font-medium">
|
||||
Presence Penalty <small className="opacity-40">(default: 0)</small>
|
||||
</Label>
|
||||
<InputNumber
|
||||
id="pres-penalty-int"
|
||||
disabled={readonly}
|
||||
value={presP}
|
||||
onChange={value => setPresP(value)}
|
||||
onChange={(value) => setPresP(value)}
|
||||
max={2}
|
||||
min={-2}
|
||||
step={0.01}
|
||||
@@ -249,7 +232,7 @@ function Settings(props) {
|
||||
<Slider
|
||||
disabled={readonly}
|
||||
value={[presP]}
|
||||
onValueChange={value => setPresP(value[0])}
|
||||
onValueChange={(value) => setPresP(value[0])}
|
||||
doubleClickHandler={() => setPresP(0)}
|
||||
max={2}
|
||||
min={-2}
|
||||
@@ -257,14 +240,11 @@ function Settings(props) {
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover
|
||||
type="pres"
|
||||
side="left"
|
||||
/>
|
||||
<OptionHover type="pres" side="left" />
|
||||
</HoverCard>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,24 +33,18 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }) => {
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTemplate
|
||||
title="Save As Preset"
|
||||
main={
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="chatGptLabel"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
<Label htmlFor="chatGptLabel" className="text-left text-sm font-medium">
|
||||
Preset Name
|
||||
</Label>
|
||||
<Input
|
||||
id="chatGptLabel"
|
||||
value={title || ''}
|
||||
onChange={e => setTitle(e.target.value || '')}
|
||||
onChange={(e) => setTitle(e.target.value || '')}
|
||||
placeholder="Set a custom name, in case you can find this preset"
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
|
||||
@@ -2,13 +2,15 @@ import React from 'react';
|
||||
|
||||
import OpenAISettings from './OpenAI/Settings.jsx';
|
||||
import BingAISettings from './BingAI/Settings.jsx';
|
||||
import GoogleSettings from './Google/Settings.jsx';
|
||||
|
||||
// A preset dialog to show readonly preset values.
|
||||
const Settings = ({ preset, ...props }) => {
|
||||
const renderSettings = () => {
|
||||
const { endpoint } = preset || {};
|
||||
// console.log('preset', preset);
|
||||
|
||||
if (endpoint === 'openAI')
|
||||
if (endpoint === 'openAI') {
|
||||
return (
|
||||
<OpenAISettings
|
||||
model={preset?.model}
|
||||
@@ -21,7 +23,7 @@ const Settings = ({ preset, ...props }) => {
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
else if (endpoint === 'bingAI')
|
||||
} else if (endpoint === 'bingAI') {
|
||||
return (
|
||||
<BingAISettings
|
||||
toneStyle={preset?.toneStyle}
|
||||
@@ -31,7 +33,24 @@ const Settings = ({ preset, ...props }) => {
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
else return <div className="text-black dark:text-white">Not implemented</div>;
|
||||
} else if (endpoint === 'google') {
|
||||
return (
|
||||
<GoogleSettings
|
||||
model={preset?.model}
|
||||
modelLabel={preset?.modelLabel}
|
||||
promptPrefix={preset?.promptPrefix}
|
||||
examples={preset?.examples}
|
||||
temperature={preset?.temperature}
|
||||
topP={preset?.topP}
|
||||
topK={preset?.topK}
|
||||
maxOutputTokens={preset?.maxOutputTokens}
|
||||
edit={true}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <div className="text-black dark:text-white">Not implemented</div>;
|
||||
}
|
||||
};
|
||||
|
||||
return renderSettings();
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Settings2 } from 'lucide-react';
|
||||
export default function AdjustButton({ onClick }) {
|
||||
const clickHandler = e => {
|
||||
const clickHandler = (e) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
};
|
||||
return (
|
||||
<button
|
||||
onClick={clickHandler}
|
||||
className="group absolute bottom-11 right-0 flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500 lg:bottom-0 lg:-right-11"
|
||||
className="group absolute bottom-11 right-0 flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500 lg:-right-11 lg:bottom-0"
|
||||
>
|
||||
<div className="m-1 mr-0 rounded-md p-2 pt-[10px] pb-[10px] group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
|
||||
<div className="m-1 mr-0 rounded-md p-2 pb-[10px] pt-[10px] group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
|
||||
<Settings2 size="1em" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user