Compare commits

..

38 Commits

Author SHA1 Message Date
Daniel D Orlando
bb3c28b723 fix: use cjs extension for commonjs files 2023-05-20 08:54:58 -07:00
Danny Avila
5964b71e14 Setup tests with new user system (#344)
* chore(.gitignore): add auth.json to gitignore
test(landing.spec.js): remove commented out code and add check for landing page title
test(login.spec.js): add test for login page title
feat(package.json): add e2e:auth script to generate auth.json storage file for e2e tests

* test(landing.spec.js): add beforeEach hook to create a new browser context with auth.json storage state
test(landing.spec.js): change test name from 'landing page' to 'Landing title'
fix(package.json): change e2e:auth script to save auth.json in e2e directory
2023-05-20 09:00:45 -04:00
Danny Avila
8c7ad09977 style(mobile.css): decrease z-index of .nav-mask to 35 (#337) 2023-05-19 21:09:07 -04:00
Danny Avila
cef2668f53 style(NewConversationMenu): add z-index to Dialog and DropdownMenuContent (#335)
style(mobile.css): decrease z-index of .nav to 40
2023-05-19 19:58:53 -04:00
Danny Avila
ab7cfc6041 Hotfix (#334)
* style(NavLinks.jsx): add 'as="div"' to Menu.Item components
refactor(Nav.jsx): remove unused code and add isMobile function to check if user is on mobile device

* conditionally render menuitem with search

---------

Co-authored-by: stunt_pilot <twitchstuntpilot@gmail.com>
2023-05-19 19:37:56 -04:00
Danny Avila
a9444b66a1 Release 0.4.6 (#332) 2023-05-19 16:21:45 -04:00
Danny Avila
ec561fcd7f Fixes all Nav Menu related errors and bugs (#331)
* chore(client): update lucide-react package to version 0.220.0
style(client): change color of MessageHeader component text to gray-500
style(client): change color of nav-close-button to gray-400 and nav-open-button to gray-500
feat(client): add Panel component to replace svg icons in Nav component

* fix: forwardRef errors in Nav Menu

* refactor(SearchBar.jsx): change clearSearch prop destructuring to props destructuring
refactor(SearchBar.jsx): add ref prop to SearchBar component
refactor(getIcon.jsx): remove unused imports
refactor(getIcon.jsx): add nullish coalescing operator to user.name and user.avatar properties

* fix (NavLinks): modals no longer close on nav menu close

* style(ExportModel.jsx): remove unnecessary z-index property from a div element

* style(ExportModel.jsx): remove trailing whitespace in input element

* refactor(Message.jsx): remove unused cancelled variable
fix(Message.jsx): fix error message length exceeding 512 characters
refactor(MenuItem.jsx): remove unused MenuItem component
2023-05-19 16:02:41 -04:00
Anirudh
ee2b3e4fb2 Refactor UI styles & configurations (#324)
* Refactor UI styles & configurations

-  Modify button styles and their color schemes to create a consistent user experience when interacting with buttons.
-  Adjust the design of the search bar to a more user-friendly layout by changing its background color and styling.
-  Create a responsive mobile behavior for the navigation bar to hide it behind a menu icon instead of permanently displaying it.

* Update .gitignore to exclude unnecessary files for Meilisearch

Update .gitignore to exclude meilisearch.exe and data.ms/*, which are not necessary for Meilisearch.

* feat: Add getCurrentBreakpoint function to get current breakpoint

This commit adds a getCurrentBreakpoint function to determine the current breakpoint of the viewport. The function uses fullConfig to determine the biggest breakpoint value of the window, and returns the corresponding breakpoint. It also updates the useEffect function to use getCurrentBreakpoint instead of checking if the userAgent matches a mobile regex.

* Update tailwind import path in Nav component

The import path for the tailwind config was updated in the Nav component to match the new project structure. This ensures that the correct Tailwind styles are applied to the component and improves maintainability.

* Add ThemeContext and cn utility function to Nav component

This commit adds the ThemeContext and cn utility function to the Nav component's dependencies with useContext and import respectively. It also modifies a class name with a ternary operator that toggles based on the theme value passed via ThemeContext.

* Update Nav button styles for better visibility

Changed the button styles for the Nav close and open buttons to enhance visibility. The text color for both buttons will now change when hovering to gray and gray-600 respectively.

* Improve message header styles and add transition effects

This commit updates the MessageHeader component styles by adjusting the text color, as well as adding transition effects to enhance the hover experience. The commit also tweaks mobile styles by adding a transition effect to `.nav` when resizing the window to mobile size.

* Refactor the message header component styling for better visual contrast

The message header component was refactored to improve its visual contrast by changing the text color for better readability. The styles of the component were modified to improve hover behavior as well as transition effects. The setSaveAsDialogShow method was shifted to the onClick prop to only execute when the endpoint is not 'chatGPTBrowser'.

* refactor: Update styling of MessageHeader and Nav buttons

The commit message describes changes made to the MessageHeader and Nav components. It summarizes the code changes as a refactor of the CSS styling for the buttons in both components, specifically updating the text and hover colors for the dark and light themes.
2023-05-19 10:51:34 -04:00
Danny Avila
67716f0d2d fix(auth.service.js): fixes deprecated error callback in mongoose save method (#323) 2023-05-18 20:08:35 -04:00
Danny Avila
e56d90e45a fix(User.js, auth.service.js, localStrategy.js): change deprecated Joi.validate() to schema.validate() method (#322) 2023-05-18 17:39:06 -04:00
Danny Avila
92eee52c52 feat (presets): hide/show endpoints, increase preset menu size in general and dynamic to endpoints (#320) 2023-05-18 16:01:16 -04:00
Danny Avila
f4d995be4c chore: dependabot updates (#319) 2023-05-18 15:32:03 -04:00
dependabot[bot]
fbdfbdd620 npm all prod(deps): bump react-router-dom from 6.11.1 to 6.11.2 (#308)
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.11.1 to 6.11.2.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.11.2/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-18 15:28:36 -04:00
dependabot[bot]
fec733e10b npm client dev(deps-dev): bump source-map-loader in /client (#307)
Bumps [source-map-loader](https://github.com/webpack-contrib/source-map-loader) from 1.1.3 to 4.0.1.
- [Release notes](https://github.com/webpack-contrib/source-map-loader/releases)
- [Changelog](https://github.com/webpack-contrib/source-map-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/source-map-loader/compare/v1.1.3...v4.0.1)

---
updated-dependencies:
- dependency-name: source-map-loader
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-18 15:28:19 -04:00
dependabot[bot]
23905dd344 npm all prod(deps): bump jake from 10.8.5 to 10.8.6 (#306)
Bumps [jake](https://github.com/jakejs/jake) from 10.8.5 to 10.8.6.
- [Changelog](https://github.com/jakejs/jake/blob/main/changelog.md)
- [Commits](https://github.com/jakejs/jake/compare/v10.8.5...v10.8.6)

---
updated-dependencies:
- dependency-name: jake
  dependency-type: indirect
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-18 15:27:58 -04:00
dependabot[bot]
ec13d74b84 npm client prod(deps): bump class-variance-authority in /client (#304)
Bumps [class-variance-authority](https://github.com/joe-bell/cva) from 0.4.0 to 0.6.0.
- [Release notes](https://github.com/joe-bell/cva/releases)
- [Commits](https://github.com/joe-bell/cva/compare/v0.4.0...v0.6.0)

---
updated-dependencies:
- dependency-name: class-variance-authority
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-18 15:27:40 -04:00
dependabot[bot]
231906161b npm client dev(deps-dev): bump typescript from 4.9.5 to 5.0.4 in /client (#303)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.9.5 to 5.0.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.9.5...v5.0.4)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-18 15:27:20 -04:00
dependabot[bot]
5c787035e5 npm all prod(deps): bump remark-parse from 10.0.1 to 10.0.2 (#302)
Bumps [remark-parse](https://github.com/remarkjs/remark) from 10.0.1 to 10.0.2.
- [Release notes](https://github.com/remarkjs/remark/releases)
- [Changelog](https://github.com/remarkjs/remark/blob/main/changelog.md)
- [Commits](https://github.com/remarkjs/remark/compare/remark-parse@10.0.1...remark-parse@10.0.2)

---
updated-dependencies:
- dependency-name: remark-parse
  dependency-type: indirect
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-18 15:27:00 -04:00
dependabot[bot]
2694690ed0 npm all prod(deps): bump cross-fetch from 3.1.5 to 3.1.6 (#301)
Bumps [cross-fetch](https://github.com/lquixada/cross-fetch) from 3.1.5 to 3.1.6.
- [Release notes](https://github.com/lquixada/cross-fetch/releases)
- [Changelog](https://github.com/lquixada/cross-fetch/blob/v3.1.6/CHANGELOG.md)
- [Commits](https://github.com/lquixada/cross-fetch/compare/v3.1.5...v3.1.6)

---
updated-dependencies:
- dependency-name: cross-fetch
  dependency-type: indirect
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-18 15:26:39 -04:00
dependabot[bot]
562bf8c920 npm api prod(deps): bump joi from 14.3.1 to 17.9.2 in /api (#300)
Bumps [joi](https://github.com/hapijs/joi) from 14.3.1 to 17.9.2.
- [Commits](https://github.com/hapijs/joi/compare/v14.3.1...v17.9.2)

---
updated-dependencies:
- dependency-name: joi
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-18 15:26:14 -04:00
dependabot[bot]
2781154df3 npm all prod(deps): bump mongoose from 6.11.1 to 7.1.1 (#299)
Bumps [mongoose](https://github.com/Automattic/mongoose) from 6.11.1 to 7.1.1.
- [Release notes](https://github.com/Automattic/mongoose/releases)
- [Changelog](https://github.com/Automattic/mongoose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Automattic/mongoose/compare/6.11.1...7.1.1)

---
updated-dependencies:
- dependency-name: mongoose
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-18 15:25:55 -04:00
dependabot[bot]
691b6d9029 npm api prod(deps): bump mongoose from 6.11.1 to 7.1.1 in /api (#298)
Bumps [mongoose](https://github.com/Automattic/mongoose) from 6.11.1 to 7.1.1.
- [Release notes](https://github.com/Automattic/mongoose/releases)
- [Changelog](https://github.com/Automattic/mongoose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Automattic/mongoose/compare/6.11.1...7.1.1)

---
updated-dependencies:
- dependency-name: mongoose
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-18 15:25:36 -04:00
dependabot[bot]
c6c3054c22 npm client prod(deps): bump filenamify from 5.1.1 to 6.0.0 in /client (#297)
Bumps [filenamify](https://github.com/sindresorhus/filenamify) from 5.1.1 to 6.0.0.
- [Release notes](https://github.com/sindresorhus/filenamify/releases)
- [Commits](https://github.com/sindresorhus/filenamify/compare/v5.1.1...v6.0.0)

---
updated-dependencies:
- dependency-name: filenamify
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-18 15:25:17 -04:00
Danny Avila
d71b61ad71 minor fixes (#318)
* refactor(SearchBar.jsx): extract onChange function to a separate function and add onKeyDown event listener to prevent spacebar from propagating

* refactor(SearchBar.jsx): extract onChange function to a separate function and add onKeyDown event listener to prevent spacebar from propagating

* refactor(SearchBar.jsx): remove unused React import statement
2023-05-18 15:22:48 -04:00
Dan Orlando
47533736e3 fix: turn off react-in-jsx-scope rule (#317) 2023-05-18 15:12:19 -04:00
Dan Orlando
a17b878617 refactor: reformat files to require parens around params (#316) 2023-05-18 14:44:07 -04:00
dependabot[bot]
91ef4872d6 npm api prod(deps): bump meilisearch from 0.31.1 to 0.32.3 in /api (#296)
Bumps [meilisearch](https://github.com/meilisearch/meilisearch-js) from 0.31.1 to 0.32.3.
- [Release notes](https://github.com/meilisearch/meilisearch-js/releases)
- [Commits](https://github.com/meilisearch/meilisearch-js/compare/v0.31.1...v0.32.3)

---
updated-dependencies:
- dependency-name: meilisearch
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-18 14:30:23 -04:00
Danny Avila
c1ddd07166 chore(EditPresetDialog.jsx): fix formatting and linting issues
feat(Settings.jsx): change max-height of the settings dialog to fit the content better
2023-05-18 14:20:41 -04:00
Dan Orlando
7fdc862042 Build/Refactor: lint pre-commit hook and reformat repo to spec (#314)
* build/refactor: move lint/prettier packages to project root, install husky, add pre-commit hook

* refactor: reformat files

* build: put full eslintrc back with all rules
2023-05-18 14:09:31 -04:00
Danny Avila
8d75b25104 Fixes (#313)
* refactor(endpoints.js): remove console.log statement
refactor(index.html): change title to "ChatGPT Clone"

* feat(Chat.jsx): set document title to conversation title or VITE_APP_TITLE or 'Chat' if conversation is null
2023-05-18 07:45:07 -04:00
Danny Avila
26152d7e5f feat(api): add support for user-provided OpenAI API key (#311)
- Add support for user-provided OpenAI API key by setting OPENAI_KEY to
  "user_provided" in .env.example
- Pass oaiApiKey to titleConvo function in titleConvo.js
- Pass oaiApiKey to askClient function in askOpenAI.js
- Modify openAI object in endpoints.js to include userProvide property
  based on whether OPENAI_KEY is set to "user_provided" or not.
2023-05-17 21:58:56 -04:00
John Chen
61a4231feb fix duplicate instructions (#310) 2023-05-17 20:18:50 -04:00
Olivier Contant
c9b035a0bd Docs/security guideline (#295)
* Create dependabot.yml

Initial dependabot.yml

* Create SECURITY.md

Guideline for security researcher to report vulnerabilities and communicate the discovery to our project community.

* Update SECURITY.md

Change wording for Discord channel initial contact and added Github Issues guideline.
2023-05-17 19:23:58 -04:00
dncc89
44ea3601c9 feat: Frontend app title environment variable (#291)
* Add app name change support

* fix indentation
2023-05-17 19:23:13 -04:00
Pawan Kumar
782a899ab3 calculate and add token usage to streaming chat (#287) 2023-05-17 19:22:35 -04:00
Anirudh
14104b276f Added functionality to allow users to set custom api keys (#276)
* Added functionality to allow users to set custom api keys

* Added error handling

* Changed token to apiKey

* Changed apiKey to oaiApiKey

* added azure openai ui

* Removed logging

* Changed configure to Use

* Made checked position more rounded

* Made setting api key optional if it is openai

* Modified error handling

* Add support for insufficient_quota errors

* Fixed faulty error detection

* removed logging
2023-05-17 19:21:30 -04:00
Danny Avila
08f3a77d58 Update README.md 2023-05-17 14:48:47 -04:00
Danny Avila
ca26732cb8 Update docker_install.md 2023-05-17 09:58:10 -04:00
179 changed files with 5922 additions and 3087 deletions

114
.eslintrc.cjs Normal file
View 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
View 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"

2
.gitignore vendored
View File

@@ -64,4 +64,4 @@ src/style - official.css
# meilisearch
meilisearch
data.ms/*
auth.json

5
.husky/pre-commit Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

19
.prettierrc.cjs Normal file
View 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,
};

View File

@@ -67,37 +67,7 @@
---
<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)
⚠️ **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>
[Past Updates](CHANGELOG.md)
##
## [Read all Latest Updates here](CHANGELOG.md)
<h1>Table of Contents</h1>

47
SECURITY.md Normal file
View 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.

View File

@@ -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*.

View File

@@ -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'
}
};

View File

@@ -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"
}

View File

@@ -23,7 +23,8 @@ const askBing = async ({
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,

View File

@@ -18,9 +18,11 @@ const browserClient = async ({
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,

View File

@@ -1,12 +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,
@@ -23,6 +26,11 @@ 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,
@@ -37,14 +45,14 @@ const askClient = async ({
},
chatGptLabel,
promptPrefix,
proxy: process.env.PROXY || null,
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;
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,
@@ -60,8 +68,25 @@ const askClient = async ({
...(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 };

View File

@@ -3,7 +3,10 @@ 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 {
encoding_for_model: encodingForModel,
get_encoding: getEncoding
} = require('@dqbd/tiktoken');
const tokenizersCache = {};
@@ -42,7 +45,7 @@ class GoogleAgent {
}
this.options.examples = this.options.examples.filter(
obj => obj.input.content !== '' && obj.output.content !== ''
(obj) => obj.input.content !== '' && obj.output.content !== ''
);
const modelOptions = this.options.modelOptions || {};
@@ -65,7 +68,8 @@ class GoogleAgent {
// 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;
this.maxPromptTokens =
this.options.maxPromptTokens || this.maxContextTokens - this.maxResponseTokens;
if (this.maxPromptTokens + this.maxResponseTokens > this.maxContextTokens) {
throw new Error(
@@ -291,7 +295,10 @@ class GoogleAgent {
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 || '';
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
@@ -368,7 +375,7 @@ class GoogleAgent {
let currentMessageId = parentMessageId;
while (currentMessageId) {
// eslint-disable-next-line no-loop-func
const message = messages.find(m => m.messageId === currentMessageId);
const message = messages.find((m) => m.messageId === currentMessageId);
if (!message) {
break;
}
@@ -380,7 +387,7 @@ class GoogleAgent {
return [];
}
return orderedMessages.map(msg => ({
return orderedMessages.map((msg) => ({
isCreatedByUser: msg.isCreatedByUser,
content: msg.text
}));

View File

@@ -16,10 +16,7 @@ class TextStream extends Readable {
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 chunkSize = Math.min(this.randomInt(minChunkSize, maxChunkSize + 1), remainingChars);
const chunk = this.text.slice(this.currentIndex, this.currentIndex + chunkSize);
this.push(chunk);

View File

@@ -17,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;
@@ -50,7 +50,7 @@ const titleConvo = async ({ endpoint, text, response }) => {
frequency_penalty: 0
};
let apiKey = process.env.OPENAI_KEY;
let apiKey = oaiApiKey || process.env.OPENAI_KEY;
if (azure) {
apiKey = process.env.AZURE_OPENAI_API_KEY;

View File

@@ -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');
}

View File

@@ -11,4 +11,4 @@ const getCitations = (res) => {
return links.join('\n');
};
module.exports = getCitations;
module.exports = getCitations;

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -57,7 +57,7 @@ module.exports = {
// will handle a syncing solution soon
const deletedConvoIds = [];
convoIds.forEach(convo =>
convoIds.forEach((convo) =>
promises.push(
Conversation.findOne({
user,
@@ -120,7 +120,7 @@ module.exports = {
},
deleteConvos: async (user, filter) => {
let toRemove = await Conversation.find({ ...filter, user }).select('conversationId');
const ids = toRemove.map(instance => instance.conversationId);
const ids = toRemove.map((instance) => instance.conversationId);
let deleteCount = await Conversation.deleteMany({ ...filter, user }).exec();
deleteCount.messages = await deleteMessages({ conversationId: { $in: ids } });
return deleteCount;

View File

@@ -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.');

View File

@@ -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;
}

View File

@@ -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' };
}
}
}
};

View File

@@ -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);

View File

@@ -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();

View File

@@ -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);

View File

@@ -1,6 +1,6 @@
{
"name": "chat-backend",
"version": "0.4.5",
"version": "0.4.6",
"description": "",
"main": "server/index.js",
"scripts": {
@@ -35,13 +35,13 @@
"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",
"openai": "^3.1.0",
"passport": "^0.6.0",

View File

@@ -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
};

View File

@@ -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(' ');

View File

@@ -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}`);
});
})();

View File

@@ -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,

View File

@@ -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, {

View File

@@ -27,7 +27,7 @@ router.post('/', requireJwtAuth, async (req, res) => {
};
const availableModels = ['chat-bison', 'text-bison'];
if (availableModels.find(model => model === endpointOption.modelOptions.model) === undefined) {
if (availableModels.find((model) => model === endpointOption.modelOptions.model) === undefined) {
return handleError(res, { text: `Illegal request: model` });
}

View File

@@ -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

View File

@@ -6,7 +6,7 @@ const {
loginController,
logoutController,
refreshController,
registrationController,
registrationController
} = require('../controllers/auth.controller');
const requireJwtAuth = require('../../middleware/requireJwtAuth');
const requireLocalAuth = require('../../middleware/requireLocalAuth');

View File

@@ -36,12 +36,14 @@ router.get('/', async function (req, res) {
}
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 || process.env.AZURE_OPENAI_API_KEY
? { availableModels: getOpenAIModels() }
key || palmUser
? { userProvide: palmUser, availableModels: ['chat-bison', 'text-bison'] }
: false;
const azureOpenAI = !!process.env.AZURE_OPENAI_KEY;
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;

View File

@@ -19,5 +19,5 @@ module.exports = {
auth,
oauth,
tokenizer,
endpoints,
endpoints
};

View File

@@ -61,4 +61,4 @@ router.get(
}
);
module.exports = router;
module.exports = router;

View File

@@ -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);

View File

@@ -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);
});

View File

@@ -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
};

View File

@@ -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',

View File

@@ -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);
}
}

View File

@@ -21,4 +21,4 @@ const registerSchema = Joi.object().keys({
module.exports = {
loginSchema,
registerSchema
};
};

View File

@@ -3,7 +3,8 @@ const pino = require('pino');
const logger = pino({
level: 'info',
redact: {
paths: [ // List of Paths to redact from the logs (https://getpino.io/#/docs/redaction)
paths: [
// List of Paths to redact from the logs (https://getpino.io/#/docs/redaction)
'env.OPENAI_KEY',
'env.BINGAI_TOKEN',
'env.CHATGPT_TOKEN',
@@ -11,14 +12,16 @@ const logger = pino({
'env.GOOGLE_CLIENT_SECRET',
'env.JWT_SECRET_DEV',
'env.JWT_SECRET_PROD',
'newUser.password'], // See example to filter object class instances
censor: '***', // Redaction character
},
'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,
const redactPatterns = [
// Array of regular expressions for redacting patterns
/api[-_]?key/i,
/password/i,
/token/i,
/secret/i,
@@ -30,7 +33,7 @@ const redactPatterns = [ // Array of regular expressions for redacting patterns
/authorization[-_]?acr[-_]?values/i,
/authorization[-_]?response[-_]?mode/i,
/authorization[-_]?nonce/i
];
];
/*
// Example of redacting sensitive data from object class instances
@@ -49,16 +52,14 @@ const redactPatterns = [ // Array of regular expressions for redacting patterns
*/
const levels = {
TRACE: 10,
DEBUG: 20,
INFO: 30,
WARN: 40,
ERROR: 50,
FATAL: 60
TRACE: 10,
DEBUG: 20,
INFO: 30,
WARN: 40,
ERROR: 50,
FATAL: 60
};
let level = levels.INFO;
module.exports = {
@@ -122,4 +123,3 @@ module.exports = {
}
}
};

View File

@@ -1,4 +1,8 @@
function genAzureEndpoint({ azureOpenAIApiInstanceName, azureOpenAIApiDeploymentName, azureOpenAIApiVersion }) {
function genAzureEndpoint({
azureOpenAIApiInstanceName,
azureOpenAIApiDeploymentName,
azureOpenAIApiVersion
}) {
return `https://${azureOpenAIApiInstanceName}.openai.azure.com/openai/deployments/${azureOpenAIApiDeploymentName}/chat/completions?api-version=${azureOpenAIApiVersion}`;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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:
###########################

View File

@@ -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",
}
}

View File

@@ -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"
}

View File

@@ -1,6 +1,6 @@
{
"name": "chat-frontend",
"version": "0.4.5",
"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,17 +43,17 @@
"@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",
@@ -60,7 +61,7 @@
"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",
@@ -97,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"
}

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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';

View File

@@ -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] rounded-r-md" />
<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>
);

View File

@@ -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>
);

View File

@@ -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={

View File

@@ -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;
}

View File

@@ -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} />
);
})}
</>

View File

@@ -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,

View File

@@ -27,12 +27,12 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
const availableEndpoints = useRecoilValue(store.availableEndpoints);
const endpointsConfig = useRecoilValue(store.endpointsConfig);
const triggerExamples = () => setShowExamples(prev => !prev);
const triggerExamples = () => setShowExamples((prev) => !prev);
const setOption = param => newValue => {
const setOption = (param) => (newValue) => {
let update = {};
update[param] = newValue;
setPreset(prevState =>
setPreset((prevState) =>
cleanupPreset({
preset: {
...prevState,
@@ -50,7 +50,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
currentExample[type] = { content: newValue };
current[i] = currentExample;
update.examples = current;
setPreset(prevState =>
setPreset((prevState) =>
cleanupPreset({
preset: {
...prevState,
@@ -66,7 +66,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
let current = preset?.examples.slice() || [];
current.push({ input: { content: '' }, output: { content: '' } });
update.examples = current;
setPreset(prevState =>
setPreset((prevState) =>
cleanupPreset({
preset: {
...prevState,
@@ -82,7 +82,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
let current = preset?.examples.slice() || [];
if (current.length <= 1) {
update.examples = [{ input: { content: '' }, output: { content: '' } }];
setPreset(prevState =>
setPreset((prevState) =>
cleanupPreset({
preset: {
...prevState,
@@ -95,7 +95,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
}
current.pop();
update.examples = current;
setPreset(prevState =>
setPreset((prevState) =>
cleanupPreset({
preset: {
...prevState,
@@ -115,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);
});
};
@@ -131,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"
@@ -145,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,
@@ -163,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
@@ -194,11 +186,9 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset, title }) => {
</div>
<div className="my-4 w-full border-t border-gray-300 dark:border-gray-500" />
<div className="w-full p-0">
{((preset?.endpoint === 'google' && !showExamples) || preset?.endpoint !== 'google') && (
<Settings
preset={_preset}
setOption={setOption}
/>
{((preset?.endpoint === 'google' && !showExamples) ||
preset?.endpoint !== 'google') && (
<Settings preset={preset} setOption={setOption} />
)}
{preset?.endpoint === 'google' && showExamples && (
<Examples
@@ -224,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>
</>

View File

@@ -22,10 +22,10 @@ const EndpointOptionsDialog = ({ open, onOpenChange, preset: _preset, title }) =
setEndpointName('PaLM');
}
const setOption = param => newValue => {
const setOption = (param) => (newValue) => {
let update = {};
update[param] = newValue;
setPreset(prevState => ({
setPreset((prevState) => ({
...prevState,
...update
}));
@@ -49,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'} - ${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>
}
@@ -79,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>
</>

View File

@@ -12,10 +12,7 @@ function Examples({ readonly, examples, setExample, addExample, removeExample, e
return (
<>
<div className={`${maxHeight} overflow-y-auto`}>
<div
id="examples-grid"
className="grid gap-6 sm:grid-cols-2"
>
<div id="examples-grid" className="grid gap-6 sm:grid-cols-2">
{examples.map((example, idx) => (
<React.Fragment key={idx}>
{/* Input */}
@@ -25,17 +22,14 @@ function Examples({ readonly, examples, setExample, addExample, removeExample, e
} 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"
>
<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)}
onChange={(e) => setExample(idx, 'input', e.target.value || null)}
placeholder="Set example input. Example is ignored if empty."
className={cn(
defaultTextProps,
@@ -52,17 +46,14 @@ function Examples({ readonly, examples, setExample, addExample, removeExample, e
} 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"
>
<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)}
onChange={(e) => setExample(idx, 'output', e.target.value || null)}
placeholder={`Set example output. Example is ignored if empty.`}
className={cn(
defaultTextProps,

View File

@@ -5,7 +5,8 @@ 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."
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 }) {

View File

@@ -1,3 +1,4 @@
import React from 'react';
import { useRecoilValue } from 'recoil';
import TextareaAutosize from 'react-textarea-autosize';
import SelectDropDown from '../../ui/SelectDropDown';
@@ -17,8 +18,19 @@ const optionText =
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-[233px]' : 'max-h-[350px]';
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');
@@ -49,17 +61,14 @@ function Settings(props) {
/>
</div>
<div className="grid w-full items-center gap-2">
<Label
htmlFor="modelLabel"
className="text-left text-sm font-medium"
>
<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)}
onChange={(e) => setModelLabel(e.target.value || null)}
placeholder="Set a custom name for PaLM2"
className={cn(
defaultTextProps,
@@ -68,17 +77,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 or context. Ignored if empty."
className={cn(
defaultTextProps,
@@ -91,17 +97,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: 0.2)</small>
</Label>
<InputNumber
id="temp-int"
disabled={readonly}
value={temperature}
onChange={value => setTemperature(value)}
onChange={(value) => setTemperature(value)}
max={1}
min={0}
step={0.01}
@@ -118,7 +121,7 @@ function Settings(props) {
<Slider
disabled={readonly}
value={[temperature]}
onValueChange={value => setTemperature(value[0])}
onValueChange={(value) => setTemperature(value[0])}
doubleClickHandler={() => setTemperature(1)}
max={1}
min={0}
@@ -126,25 +129,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: 0.95)</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 +158,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 +166,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="top-k-int"
className="text-left text-sm font-medium"
>
<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)}
onChange={(value) => setTopK(value)}
max={40}
min={1}
step={0.01}
@@ -205,7 +196,7 @@ function Settings(props) {
<Slider
disabled={readonly}
value={[topK]}
onValueChange={value => setTopK(value[0])}
onValueChange={(value) => setTopK(value[0])}
doubleClickHandler={() => setTopK(0)}
max={40}
min={1}
@@ -213,26 +204,20 @@ function Settings(props) {
className="flex h-4 w-full"
/>
</HoverCardTrigger>
<OptionHover
type="topk"
side="left"
/>
<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"
>
<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)}
onChange={(value) => setMaxOutputTokens(value)}
max={1024}
min={1}
step={1}
@@ -249,7 +234,7 @@ function Settings(props) {
<Slider
disabled={readonly}
value={[maxOutputTokens]}
onValueChange={value => setMaxOutputTokens(value[0])}
onValueChange={(value) => setMaxOutputTokens(value[0])}
doubleClickHandler={() => setMaxOutputTokens(0)}
max={1024}
min={1}
@@ -257,10 +242,7 @@ function Settings(props) {
className="flex h-4 w-full"
/>
</HoverCardTrigger>
<OptionHover
type="maxoutputtokens"
side="left"
/>
<OptionHover type="maxoutputtokens" side="left" />
</HoverCard>
</div>
</div>

View File

@@ -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);
@@ -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,10 +240,7 @@ function Settings(props) {
className="flex h-4 w-full"
/>
</HoverCardTrigger>
<OptionHover
type="pres"
side="left"
/>
<OptionHover type="pres" side="left" />
</HoverCard>
</div>
</div>

View File

@@ -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,

View File

@@ -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>

View File

@@ -21,7 +21,7 @@ function BingAIOptions({ show }) {
if (endpoint !== 'bingAI') return null;
if (conversationId !== 'new' && !show) return null;
const triggerAdvancedMode = () => setAdvancedMode(prev => !prev);
const triggerAdvancedMode = () => setAdvancedMode((prev) => !prev);
const switchToSimpleMode = () => {
setAdvancedMode(false);
@@ -31,10 +31,10 @@ function BingAIOptions({ show }) {
setSaveAsDialogShow(true);
};
const setOption = param => newValue => {
const setOption = (param) => (newValue) => {
let update = {};
update[param] = newValue;
setConversation(prevState => ({
setConversation((prevState) => ({
...prevState,
...update
}));
@@ -44,8 +44,11 @@ function BingAIOptions({ show }) {
'transition-colors shadow-md rounded-md min-w-[75px] font-normal bg-white border-black/10 hover:border-black/10 focus:border-black/10 dark:border-black/10 dark:hover:border-black/10 dark:focus:border-black/10 border dark:bg-gray-700 text-black dark:text-white';
const defaultClasses =
'p-2 rounded-md min-w-[75px] font-normal bg-white/[.60] dark:bg-gray-700 text-black text-xs';
const defaultSelected = cn(defaultClasses, 'font-medium data-[state=active]:text-white text-xs text-white');
const selectedClass = val => val + '-tab ' + defaultSelected;
const defaultSelected = cn(
defaultClasses,
'font-medium data-[state=active]:text-white text-xs text-white'
);
const selectedClass = (val) => val + '-tab ' + defaultSelected;
return (
<>
@@ -58,7 +61,7 @@ function BingAIOptions({ show }) {
<SelectDropDown
title="Mode"
value={jailbreak ? 'Sydney' : 'BingAI'}
setValue={value => setOption('jailbreak')(value === 'Sydney')}
setValue={(value) => setOption('jailbreak')(value === 'Sydney')}
availableValues={['BingAI', 'Sydney']}
showAbove={true}
showLabel={false}
@@ -75,7 +78,7 @@ function BingAIOptions({ show }) {
cardStyle +
' z-50 flex h-[40px] flex-none items-center justify-center px-0 hover:bg-slate-50 dark:hover:bg-gray-600'
}
onValueChange={value => setOption('toneStyle')(value.toLowerCase())}
onValueChange={(value) => setOption('toneStyle')(value.toLowerCase())}
>
<TabsList className="bg-white/[.60] dark:bg-gray-700">
<TabsTrigger

View File

@@ -17,10 +17,10 @@ function ChatGPTOptions() {
const models = endpointsConfig?.['chatGPTBrowser']?.['availableModels'] || [];
const setOption = param => newValue => {
const setOption = (param) => (newValue) => {
let update = {};
update[param] = newValue;
setConversation(prevState => ({
setConversation((prevState) => ({
...prevState,
...update
}));

View File

@@ -2,17 +2,17 @@ import React from 'react';
export default function Footer() {
return (
<div className="hidden px-3 pt-2 pb-1 text-center text-xs text-black/50 dark:text-white/50 md:block md:px-4 md:pt-3 md:pb-4">
<div className="hidden px-3 pb-1 pt-2 text-center text-xs text-black/50 dark:text-white/50 md:block md:px-4 md:pb-4 md:pt-3">
<a
href="https://github.com/danny-avila/chatgpt-clone"
target="_blank"
rel="noreferrer"
className="underline"
>
ChatGPT Clone
{import.meta.env.VITE_APP_TITLE || 'ChatGPT Clone'}
</a>
. Serves and searches all conversations reliably. All AI convos under one house. Pay per call and not
per month (cents compared to dollars).
. Serves and searches all conversations reliably. All AI convos under one house. Pay per call
and not per month (cents compared to dollars).
</div>
);
}

View File

@@ -29,8 +29,8 @@ function GoogleOptions() {
const models = endpointsConfig?.['google']?.['availableModels'] || [];
const triggerAdvancedMode = () => setAdvancedMode(prev => !prev);
const triggerExamples = () => setShowExamples(prev => !prev);
const triggerAdvancedMode = () => setAdvancedMode((prev) => !prev);
const triggerExamples = () => setShowExamples((prev) => !prev);
const switchToSimpleMode = () => {
setAdvancedMode(false);
@@ -40,10 +40,10 @@ function GoogleOptions() {
setSaveAsDialogShow(true);
};
const setOption = param => newValue => {
const setOption = (param) => (newValue) => {
let update = {};
update[param] = newValue;
setConversation(prevState => ({
setConversation((prevState) => ({
...prevState,
...update
}));
@@ -56,7 +56,7 @@ function GoogleOptions() {
currentExample[type] = { content: newValue };
current[i] = currentExample;
update.examples = current;
setConversation(prevState => ({
setConversation((prevState) => ({
...prevState,
...update
}));
@@ -67,7 +67,7 @@ function GoogleOptions() {
let current = conversation?.examples.slice() || [];
current.push({ input: { content: '' }, output: { content: '' } });
update.examples = current;
setConversation(prevState => ({
setConversation((prevState) => ({
...prevState,
...update
}));
@@ -78,7 +78,7 @@ function GoogleOptions() {
let current = conversation?.examples.slice() || [];
if (current.length <= 1) {
update.examples = [{ input: { content: '' }, output: { content: '' } }];
setConversation(prevState => ({
setConversation((prevState) => ({
...prevState,
...update
}));
@@ -86,7 +86,7 @@ function GoogleOptions() {
}
current.pop();
update.examples = current;
setConversation(prevState => ({
setConversation((prevState) => ({
...prevState,
...update
}));

View File

@@ -12,8 +12,8 @@ const alternateName = {
azureOpenAI: 'Azure OpenAI',
bingAI: 'Bing',
chatGPTBrowser: 'ChatGPT',
google: 'PaLM',
}
google: 'PaLM'
};
export default function ModelItem({ endpoint, value, onSelect }) {
const [setTokenDialogOpen, setSetTokenDialogOpen] = useState(false);
@@ -26,7 +26,7 @@ export default function ModelItem({ endpoint, value, onSelect }) {
className: 'mr-2'
});
const isuserProvide = endpointsConfig?.[endpoint]?.userProvide;
const isUserProvided = endpointsConfig?.[endpoint]?.userProvide;
// regular model
return (
@@ -37,12 +37,12 @@ export default function ModelItem({ endpoint, value, onSelect }) {
>
{icon}
{alternateName[endpoint] || endpoint}
{!!['azureOpenAI', 'openAI'].find(e => e === endpoint) && <sup>$</sup>}
{!!['azureOpenAI', 'openAI'].find((e) => e === endpoint) && <sup>$</sup>}
<div className="flex w-4 flex-1" />
{isuserProvide ? (
{isUserProvided ? (
<button
className="invisible m-0 mr-1 flex-initial rounded-md p-0 text-xs font-medium text-gray-400 hover:text-gray-700 group-hover:visible dark:font-normal dark:text-gray-400 dark:hover:text-gray-200"
onClick={e => {
onClick={(e) => {
e.preventDefault();
setSetTokenDialogOpen(true);
}}

View File

@@ -4,13 +4,8 @@ import EndpointItem from './EndpointItem.jsx';
export default function EndpointItems({ endpoints, onSelect }) {
return (
<>
{endpoints.map(endpoint => (
<EndpointItem
key={endpoint}
value={endpoint}
onSelect={onSelect}
endpoint={endpoint}
/>
{endpoints.map((endpoint) => (
<EndpointItem key={endpoint} value={endpoint} onSelect={onSelect} endpoint={endpoint} />
))}
</>
);

View File

@@ -2,16 +2,23 @@ import { useState } from 'react';
import { FileUp } from 'lucide-react';
import { cn } from '~/utils/';
const FileUpload = ({ onFileSelected, successText = null, invalidText = null, validator = null, text = null, id = '1' }) => {
const FileUpload = ({
onFileSelected,
successText = null,
invalidText = null,
validator = null,
text = null,
id = '1'
}) => {
const [statusColor, setStatusColor] = useState('text-gray-600');
const [status, setStatus] = useState(null);
const handleFileChange = event => {
const handleFileChange = (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
reader.onload = (e) => {
const jsonData = JSON.parse(e.target.result);
if (validator && !validator(jsonData)) {
setStatus('invalid');
@@ -23,7 +30,7 @@ const FileUpload = ({ onFileSelected, successText = null, invalidText = null, va
setStatus('success');
setStatusColor('text-green-500 dark:text-green-500');
}
onFileSelected(jsonData);
};
reader.readAsText(file);
@@ -38,7 +45,9 @@ const FileUpload = ({ onFileSelected, successText = null, invalidText = null, va
)}
>
<FileUp className="mr-1 flex w-[22px] items-center stroke-1" />
<span className="flex text-xs ">{!status ? text || 'Import' : (status === 'success' ? successText : invalidText)}</span>
<span className="flex text-xs ">
{!status ? text || 'Import' : status === 'success' ? successText : invalidText}
</span>
<input
id={`file-upload-${id}`}
value=""

View File

@@ -4,7 +4,13 @@ import EditIcon from '../../svg/EditIcon.jsx';
import TrashIcon from '../../svg/TrashIcon.jsx';
import getIcon from '~/utils/getIcon';
export default function PresetItem({ preset = {}, value, onSelect, onChangePreset, onDeletePreset }) {
export default function PresetItem({
preset = {},
value,
onSelect,
onChangePreset,
onDeletePreset
}) {
const { endpoint } = preset;
const icon = getIcon({
@@ -53,7 +59,7 @@ export default function PresetItem({ preset = {}, value, onSelect, onChangePrese
<div className="flex w-4 flex-1" />
<button
className="invisible m-0 mr-1 rounded-md p-2 text-gray-400 hover:text-gray-700 group-hover:visible dark:text-gray-400 dark:hover:text-gray-200 "
onClick={e => {
onClick={(e) => {
e.preventDefault();
onChangePreset(preset);
}}
@@ -62,7 +68,7 @@ export default function PresetItem({ preset = {}, value, onSelect, onChangePrese
</button>
<button
className="invisible m-0 rounded-md text-gray-400 hover:text-gray-700 group-hover:visible dark:text-gray-400 dark:hover:text-gray-200 "
onClick={e => {
onClick={(e) => {
e.preventDefault();
onDeletePreset(preset);
}}

View File

@@ -4,7 +4,7 @@ import PresetItem from './PresetItem.jsx';
export default function PresetItems({ presets, onSelect, onChangePreset, onDeletePreset }) {
return (
<>
{presets.map(preset => (
{presets.map((preset) => (
<PresetItem
key={preset?.presetId}
value={preset}

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import { useState, useEffect } from 'react';
import cleanupPreset from '~/utils/cleanupPreset.js';
import { useRecoilValue, useRecoilState } from 'recoil';
import EditPresetDialog from '../../Endpoints/EditPresetDialog';
@@ -19,12 +19,14 @@ import {
} from '../../ui/DropdownMenu.tsx';
import { Dialog, DialogTrigger } from '../../ui/Dialog.tsx';
import DialogTemplate from '../../ui/DialogTemplate';
import { cn } from '~/utils/';
import store from '~/store';
export default function NewConversationMenu() {
const [menuOpen, setMenuOpen] = useState(false);
const [showPresets, setShowPresets] = useState(true);
const [showEndpoints, setShowEndpoints] = useState(true);
const [presetModelVisible, setPresetModelVisible] = useState(false);
const [preset, setPreset] = useState(false);
@@ -39,21 +41,21 @@ export default function NewConversationMenu() {
const deletePresetsMutation = useDeletePresetMutation();
const createPresetMutation = useCreatePresetMutation();
const importPreset = jsonData => {
const importPreset = (jsonData) => {
createPresetMutation.mutate(
{ ...jsonData },
{
onSuccess: data => {
onSuccess: (data) => {
setPresets(data);
},
onError: error => {
onError: (error) => {
console.error('Error uploading the preset:', error);
}
}
);
};
const onFileSelected = jsonData => {
const onFileSelected = (jsonData) => {
const jsonPreset = { ...cleanupPreset({ preset: jsonData, endpointsConfig }), presetId: null };
importPreset(jsonPreset);
};
@@ -61,7 +63,7 @@ export default function NewConversationMenu() {
// update the default model when availableModels changes
// typically, availableModels changes => modelsFilter or customGPTModels changes
useEffect(() => {
const isInvalidConversation = !availableEndpoints.find(e => e === endpoint);
const isInvalidConversation = !availableEndpoints.find((e) => e === endpoint);
if (conversationId == 'new' && isInvalidConversation) {
newConversation();
}
@@ -80,7 +82,7 @@ export default function NewConversationMenu() {
}, [conversation]);
// set the current model
const onSelectEndpoint = newEndpoint => {
const onSelectEndpoint = (newEndpoint) => {
setMenuOpen(false);
if (!newEndpoint) return;
@@ -90,7 +92,7 @@ export default function NewConversationMenu() {
};
// set the current model
const onSelectPreset = newPreset => {
const onSelectPreset = (newPreset) => {
setMenuOpen(false);
if (!newPreset) return;
else {
@@ -98,7 +100,7 @@ export default function NewConversationMenu() {
}
};
const onChangePreset = preset => {
const onChangePreset = (preset) => {
setPresetModelVisible(true);
setPreset(preset);
};
@@ -107,7 +109,7 @@ export default function NewConversationMenu() {
deletePresetsMutation.mutate({ arg: {} });
};
const onDeletePreset = preset => {
const onDeletePreset = (preset) => {
deletePresetsMutation.mutate({ arg: preset });
};
@@ -120,11 +122,8 @@ export default function NewConversationMenu() {
});
return (
<Dialog>
<DropdownMenu
open={menuOpen}
onOpenChange={setMenuOpen}
>
<Dialog className="z-[100]">
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
@@ -137,42 +136,43 @@ export default function NewConversationMenu() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="min-w-[300px] dark:bg-gray-700"
onCloseAutoFocus={event => event.preventDefault()}
className="min-w-[300px] dark:bg-gray-700 z-[100]"
onCloseAutoFocus={(event) => event.preventDefault()}
>
<DropdownMenuLabel className="dark:text-gray-300">Select an Endpoint</DropdownMenuLabel>
<DropdownMenuLabel
className="cursor-pointer dark:text-gray-300"
onClick={() => setShowEndpoints((prev) => !prev)}
>
{showEndpoints ? 'Hide ' : 'Show '} Endpoints
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={endpoint}
onValueChange={onSelectEndpoint}
className="overflow-y-auto"
>
{availableEndpoints.length ? (
<EndpointItems
endpoints={availableEndpoints}
onSelect={onSelectEndpoint}
/>
) : (
<DropdownMenuLabel className="dark:text-gray-300">No endpoint available.</DropdownMenuLabel>
)}
{showEndpoints &&
(availableEndpoints.length ? (
<EndpointItems endpoints={availableEndpoints} onSelect={onSelectEndpoint} />
) : (
<DropdownMenuLabel className="dark:text-gray-300">
No endpoint available.
</DropdownMenuLabel>
))}
</DropdownMenuRadioGroup>
<div className="mt-6 w-full" />
<div className="mt-2 w-full" />
<DropdownMenuLabel className="flex items-center dark:text-gray-300">
<span
className="cursor-pointer"
onClick={() => setShowPresets(prev => !prev)}
>
<span className="cursor-pointer mr-auto " onClick={() => setShowPresets((prev) => !prev)}>
{showPresets ? 'Hide ' : 'Show '} Presets
</span>
<div className="flex-1" />
<FileUpload onFileSelected={onFileSelected} />
<Dialog>
<DialogTrigger asChild>
<label
htmlFor="file-upload"
className=" mr-1 flex h-[32px] h-auto cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium font-normal text-gray-600 transition-colors hover:bg-slate-200 hover:text-red-700 dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-green-500"
className="mr-1 flex h-[32px] h-auto cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium font-normal text-gray-600 transition-colors hover:bg-slate-200 hover:text-red-700 dark:bg-transparent dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-green-500"
>
{/* <Button
type="button"
@@ -197,7 +197,7 @@ export default function NewConversationMenu() {
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
onValueChange={onSelectPreset}
className="max-h-[150px] overflow-y-auto"
className={cn('overflow-y-auto', showEndpoints ? 'max-h-[180px]' : 'max-h-[315px]')}
>
{showPresets &&
(presets.length ? (

View File

@@ -16,8 +16,15 @@ function OpenAIOptions() {
const [conversation, setConversation] = useRecoilState(store.conversation) || {};
const { endpoint, conversationId } = conversation;
const { model, chatGptLabel, promptPrefix, temperature, top_p, presence_penalty, frequency_penalty } =
conversation;
const {
model,
chatGptLabel,
promptPrefix,
temperature,
top_p,
presence_penalty,
frequency_penalty
} = conversation;
const endpointsConfig = useRecoilValue(store.endpointsConfig);
@@ -26,7 +33,7 @@ function OpenAIOptions() {
const models = endpointsConfig?.['openAI']?.['availableModels'] || [];
const triggerAdvancedMode = () => setAdvancedMode(prev => !prev);
const triggerAdvancedMode = () => setAdvancedMode((prev) => !prev);
const switchToSimpleMode = () => {
setAdvancedMode(false);
@@ -36,10 +43,10 @@ function OpenAIOptions() {
setSaveAsDialogShow(true);
};
const setOption = param => newValue => {
const setOption = (param) => (newValue) => {
let update = {};
update[param] = newValue;
setConversation(prevState => ({
setConversation((prevState) => ({
...prevState,
...update
}));

View File

@@ -8,9 +8,9 @@ export default function RowButton({ onClick, children, text, className }) {
type="button"
>
{children}
<span className="hidden md:block">{text}</span>
<span className="hidden md:block">{text}</span>
{/* <RegenerateIcon />
<span className="hidden md:block">Regenerate response</span> */}
</button>
);
}
}

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { Input } from '../../ui/Input.tsx';
import { Label } from '../../ui/Label.tsx';
import { cn } from '~/utils/';
function InputWithLabel({ value, onChange, label, id }) {
const defaultTextProps =
'rounded-md border border-gray-300 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.10)] 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-400 dark:bg-gray-700 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';
return (
<>
<Label htmlFor={id} className="text-left text-sm font-medium">
{label}
<br />
</Label>
<Input
id={id}
value={value || ''}
onChange={onChange}
placeholder={`Enter ${label}`}
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'
)}
/>
</>
);
}
export default InputWithLabel;

View File

@@ -4,11 +4,24 @@ import { Dialog } from '../../ui/Dialog.tsx';
import { Input } from '../../ui/Input.tsx';
import { Label } from '../../ui/Label.tsx';
import { cn } from '~/utils/';
import * as Checkbox from '@radix-ui/react-checkbox';
import { CheckIcon } from '@radix-ui/react-icons';
import FileUpload from '../NewConversationMenu/FileUpload';
import store from '~/store';
import InputWithLabel from './InputWithLabel';
function isJson(str) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
const SetTokenDialog = ({ open, onOpenChange, endpoint }) => {
const [token, setToken] = useState('');
const [showPanel, setShowPanel] = useState(false);
const { getToken, saveToken } = store.useToken(endpoint);
const defaultTextProps =
@@ -20,14 +33,24 @@ const SetTokenDialog = ({ open, onOpenChange, endpoint }) => {
};
useEffect(() => {
setToken(getToken() ?? '');
let oldToken = getToken();
if (isJson(token)) {
setShowPanel(true);
}
setToken(oldToken ?? '');
}, [open]);
useEffect(() => {
if (!showPanel && isJson(token)) {
setToken('');
}
}, [showPanel]);
const helpText = {
bingAI: (
<small className="break-all text-gray-600">
The Bing Access Token is the "_U" cookie from bing.com. Use dev tools or an extension while logged
into the site to view it.
The Bing Access Token is the "_U" cookie from bing.com. Use dev tools or an extension while
logged into the site to view it.
</small>
),
chatGPTBrowser: (
@@ -73,36 +96,45 @@ const SetTokenDialog = ({ open, onOpenChange, endpoint }) => {
>
Create a Service Account
</a>
. Make sure to click 'Create and Continue' to give at least the 'Vertex AI User' role. Lastly, create
a JSON key to import here.
. Make sure to click 'Create and Continue' to give at least the 'Vertex AI User' role.
Lastly, create a JSON key to import here.
</small>
)
};
function getAzure(name) {
if (isJson(token)) {
let newToken = JSON.parse(token);
return newToken[name];
} else {
return '';
}
}
function setAzure(name, value) {
let newToken = {};
if (isJson(token)) {
newToken = JSON.parse(token);
}
newToken[name] = value;
setToken(JSON.stringify(newToken));
}
return (
<Dialog
open={open}
onOpenChange={onOpenChange}
>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTemplate
title={`Set Token of ${endpoint}`}
main={
<div className="grid w-full items-center gap-2">
<Label
htmlFor="chatGptLabel"
className="text-left text-sm font-medium"
>
Token Name
<br />
</Label>
{endpoint === 'google' ? (
<FileUpload
id="googleKey"
id="googleKey"
className="w-full"
text="Import Service Account JSON Key"
successText="Successfully Imported Service Account JSON Key"
invalidText="Invalid Service Account JSON Key, Did you import the correct file?"
validator={credentials => {
validator={(credentials) => {
if (!credentials) {
return false;
}
@@ -133,23 +165,85 @@ const SetTokenDialog = ({ open, onOpenChange, endpoint }) => {
return true;
}}
onFileSelected={data => {
onFileSelected={(data) => {
setToken(JSON.stringify(data));
}}
/>
) : (
<Input
id="chatGptLabel"
value={token || ''}
onChange={e => setToken(e.target.value || '')}
placeholder="Set the token."
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'
) : endpoint === 'openAI' ? (
<>
{!showPanel ? (
<>
<InputWithLabel
id={'chatGPTLabel'}
value={token || ''}
onChange={(e) => setToken(e.target.value || '')}
label={'OpenAI API Key'}
/>
</>
) : (
<>
<InputWithLabel
id={'instanceNameLabel'}
value={getAzure('instanceName') || ''}
onChange={(e) => setAzure('instanceName', e.target.value || '')}
label={'Azure OpenAI Instance Name'}
/>
<InputWithLabel
id={'deploymentNameLabel'}
value={getAzure('deploymentName') || ''}
onChange={(e) => setAzure('deploymentName', e.target.value || '')}
label={'Azure OpenAI Deployment Name'}
/>
<InputWithLabel
id={'versionLabel'}
value={getAzure('version') || ''}
onChange={(e) => setAzure('version', e.target.value || '')}
label={'Azure OpenAI API Version'}
/>
<InputWithLabel
id={'apiKeyLabel'}
value={getAzure('apiKey') || ''}
onChange={(e) => setAzure('apiKey', e.target.value || '')}
label={'Azure OpenAI API Key'}
/>
</>
)}
/>
<div className="flex items-center">
<Checkbox.Root
className="flex h-[20px] w-[20px] appearance-none items-center justify-center rounded-[4px] bg-gray-100 text-white outline-none hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-900"
id="azureOpenAI"
checked={showPanel}
onCheckedChange={() => setShowPanel(!showPanel)}
>
<Checkbox.Indicator className="flex h-[20px] w-[20px] items-center justify-center rounded-[3.5px] bg-green-600">
<CheckIcon />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="pl-[8px] text-[15px] leading-none dark:text-white"
htmlFor="azureOpenAI"
>
Use Azure OpenAI.
</label>
</div>
</>
) : (
<>
<InputWithLabel
id={'chatGPTLabel'}
value={token || ''}
onChange={(e) => setToken(e.target.value || '')}
label={'Token Name'}
/>
</>
)}
<small className="text-red-600">Your token will be sent to the server, but not saved.</small>
<small className="text-red-600">
Your token will be sent to the server, but not saved.
</small>
{helpText?.[endpoint]}
</div>
}

View File

@@ -17,7 +17,7 @@ export default function SubmitButton({
const isTokenProvided = endpointsConfig?.[endpoint]?.userProvide ? !!getToken() : true;
const clickHandler = e => {
const clickHandler = (e) => {
e.preventDefault();
submitMessage();
};
@@ -33,7 +33,7 @@ export default function SubmitButton({
type="button"
className="group absolute bottom-0 right-0 flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
>
<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">
<StopGeneratingIcon />
</div>
</button>
@@ -61,7 +61,7 @@ export default function SubmitButton({
// </div>
// </button>
// );
else if (!isTokenProvided) {
else if (!isTokenProvided && endpoint !== 'openAI') {
return (
<>
<button
@@ -69,7 +69,7 @@ export default function SubmitButton({
type="button"
className="group absolute bottom-0 right-0 flex h-[100%] w-auto items-center justify-center bg-transparent p-1 text-gray-500"
>
<div className="m-1 mr-0 rounded-md p-2 pt-[10px] pb-[10px] align-middle text-xs 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] align-middle text-xs 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">
<Settings className="mr-1 inline-block w-[18px]" />
Set Token First
</div>
@@ -88,7 +88,7 @@ export default function SubmitButton({
disabled={disabled}
className="group absolute bottom-0 right-0 flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
>
<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">
<svg
stroke="currentColor"
fill="none"
@@ -101,12 +101,7 @@ export default function SubmitButton({
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="22"
y1="2"
x2="11"
y2="13"
/>
<line x1="22" y1="2" x2="11" y2="13" />
<polygon points="22 2 15 22 11 13 2 9 22 2" />
</svg>
</div>

View File

@@ -34,7 +34,7 @@ export default function TextChat({ isSearchView = false }) {
// const bingStylesRef = useRef(null);
const [showBingToneSetting, setShowBingToneSetting] = useState(false);
const isNotAppendable = (latestMessage?.unfinished & !isSubmitting) || latestMessage?.error;
const isNotAppendable = latestMessage?.unfinished & !isSubmitting || latestMessage?.error;
// auto focus to input, when enter a conversation.
useEffect(() => {
@@ -64,12 +64,12 @@ export default function TextChat({ isSearchView = false }) {
setText('');
};
const handleStopGenerating = e => {
const handleStopGenerating = (e) => {
e.preventDefault();
stopGenerating();
};
const handleKeyDown = e => {
const handleKeyDown = (e) => {
if (e.key === 'Enter' && isSubmitting) {
return;
}
@@ -83,7 +83,7 @@ export default function TextChat({ isSearchView = false }) {
}
};
const handleKeyUp = e => {
const handleKeyUp = (e) => {
if (e.keyCode === 8 && e.target.value.trim() === '') {
setText(e.target.value);
}
@@ -105,7 +105,7 @@ export default function TextChat({ isSearchView = false }) {
isComposing.current = false;
};
const changeHandler = e => {
const changeHandler = (e) => {
const { value } = e.target;
setText(value);
@@ -128,7 +128,7 @@ export default function TextChat({ isSearchView = false }) {
};
const handleBingToneSetting = () => {
setShowBingToneSetting(show => !show);
setShowBingToneSetting((show) => !show);
};
if (isSearchView) return <></>;

View File

@@ -27,7 +27,7 @@ export default function MessageHandler() {
text: data,
parentMessageId: message?.overrideParentMessageId,
messageId: message?.overrideParentMessageId + '_',
submitting: true,
submitting: true
// unfinished: true
}
]);
@@ -40,7 +40,7 @@ export default function MessageHandler() {
text: data,
parentMessageId: message?.messageId,
messageId: message?.messageId + '_',
submitting: true,
submitting: true
// unfinished: true
}
]);
@@ -72,7 +72,7 @@ export default function MessageHandler() {
}, 5000);
}
setConversation(prevState => ({
setConversation((prevState) => ({
...prevState,
...conversation
}));
@@ -104,7 +104,7 @@ export default function MessageHandler() {
]);
const { conversationId } = message;
setConversation(prevState => ({
setConversation((prevState) => ({
...prevState,
conversationId
}));
@@ -133,7 +133,7 @@ export default function MessageHandler() {
}, 5000);
}
setConversation(prevState => ({
setConversation((prevState) => ({
...prevState,
...conversation
}));
@@ -153,7 +153,7 @@ export default function MessageHandler() {
return;
};
const abortConversation = conversationId => {
const abortConversation = (conversationId) => {
console.log(submission);
const { endpoint } = submission?.conversation || {};
@@ -161,18 +161,18 @@ export default function MessageHandler() {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
abortKey: conversationId
})
})
.then(response => response.json())
.then(data => {
.then((response) => response.json())
.then((data) => {
console.log('aborted', data);
cancelHandler(data, submission);
})
.catch(error => {
.catch((error) => {
console.error('Error aborting request');
console.error(error);
// errorHandler({ text: 'Error aborting request' }, { ...submission, message });
@@ -190,10 +190,10 @@ export default function MessageHandler() {
const events = new SSE(server, {
payload: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`}
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }
});
events.onmessage = e => {
events.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.final) {
@@ -219,7 +219,8 @@ export default function MessageHandler() {
events.onopen = () => console.log('connection is opened');
events.oncancel = () => abortConversation(message?.conversationId || submission?.conversationId);
events.oncancel = () =>
abortConversation(message?.conversationId || submission?.conversationId);
events.onerror = function (e) {
console.log('error in opening conn.');

View File

@@ -7,15 +7,9 @@ const CodeBlock = ({ lang, codeChildren }) => {
return (
<div className="rounded-md bg-black">
<CodeBar
lang={lang}
codeRef={codeRef}
/>
<CodeBar lang={lang} codeRef={codeRef} />
<div className="overflow-y-auto p-4">
<code
ref={codeRef}
className={`hljs !whitespace-pre language-${lang}`}
>
<code ref={codeRef} className={`hljs !whitespace-pre language-${lang}`}>
{codeChildren}
</code>
</div>

View File

@@ -4,7 +4,7 @@ import rehypeKatex from 'rehype-katex';
import rehypeHighlight from 'rehype-highlight';
import remarkMath from 'remark-math';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw'
import rehypeRaw from 'rehype-raw';
import CodeBlock from './CodeBlock';
import { langSubset } from '~/utils/languages.mjs';
@@ -19,7 +19,7 @@ const Content = React.memo(({ content }) => {
subset: langSubset
}
],
[rehypeRaw],
[rehypeRaw]
];
return (
@@ -29,7 +29,7 @@ const Content = React.memo(({ content }) => {
linkTarget="_new"
components={{
code,
p,
p
// em,
}}
>
@@ -46,17 +46,12 @@ const code = React.memo((props) => {
if (inline) {
return <code className={className}>{children}</code>;
} else {
return (
<CodeBlock
lang={lang || 'text'}
codeChildren={children}
/>
);
return <CodeBlock lang={lang || 'text'} codeChildren={children} />;
}
});
const p = React.memo((props) => {
return <p className="whitespace-pre-wrap mb-2">{props?.children}</p>;
return <p className="mb-2 whitespace-pre-wrap">{props?.children}</p>;
});
// const blinker = ({ node }) => {

View File

@@ -3,7 +3,11 @@ import React from 'react';
export default function SubRow({ children, classes = '', subclasses = '', onClick }) {
return (
<div className={`flex justify-between ${classes}`} onClick={onClick}>
<div className={`flex items-center justify-center gap-1 self-center pt-2 text-xs ${subclasses}`}>{children}</div>
<div
className={`flex items-center justify-center gap-1 self-center pt-2 text-xs ${subclasses}`}
>
{children}
</div>
</div>
);
}

View File

@@ -18,7 +18,7 @@ export default function HoverButtons({
const branchingSupported =
// azureOpenAI, openAI, chatGPTBrowser support branching, so edit enabled
!!['azureOpenAI', 'openAI', 'chatGPTBrowser', 'google'].find(e => e === endpoint) ||
!!['azureOpenAI', 'openAI', 'chatGPTBrowser', 'google'].find((e) => e === endpoint) ||
// Sydney in bingAI supports branching, so edit enabled
(endpoint === 'bingAI' && jailbreak);
@@ -32,10 +32,14 @@ export default function HoverButtons({
// for now, once branching is supported, regerate will be enabled
const regenerateEnabled =
// !message?.error &&
!message?.isCreatedByUser && !message?.searchResult && !isEditting && !isSubmitting && branchingSupported;
!message?.isCreatedByUser &&
!message?.searchResult &&
!isEditting &&
!isSubmitting &&
branchingSupported;
return (
<div className="visible mt-2 flex justify-center gap-3 self-end text-gray-400 md:gap-4 lg:absolute lg:top-0 lg:right-0 lg:mt-0 lg:translate-x-full lg:gap-1 lg:self-center lg:pl-2">
<div className="visible mt-2 flex justify-center gap-3 self-end text-gray-400 md:gap-4 lg:absolute lg:right-0 lg:top-0 lg:mt-0 lg:translate-x-full lg:gap-1 lg:self-center lg:pl-2">
{editEnabled ? (
<button
className="hover-button rounded-md p-1 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible"

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import copy from 'copy-to-clipboard';
import SubRow from './Content/SubRow';
@@ -12,6 +12,15 @@ import { useGetConversationByIdQuery } from '~/data-provider';
import { cn } from '~/utils/';
import store from '~/store';
function isJson(str) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
export default function Message({
conversation,
message,
@@ -22,7 +31,7 @@ export default function Message({
siblingCount,
setSiblingIdx
}) {
const { text, searchResult, isCreatedByUser, error, submitting, unfinished, cancelled } = message;
const { text, searchResult, isCreatedByUser, error, submitting, unfinished } = message;
const isSubmitting = useRecoilValue(store.isSubmitting);
const setLatestMessage = useSetRecoilState(store.latestMessage);
const [abortScroll, setAbort] = useState(false);
@@ -32,7 +41,9 @@ export default function Message({
const { ask, regenerate } = useMessageHandler();
const { switchToConversation } = store.useConversation();
const blinker = submitting && isSubmitting;
const getConversationQuery = useGetConversationByIdQuery(message.conversationId, { enabled: false });
const getConversationQuery = useGetConversationByIdQuery(message.conversationId, {
enabled: false
});
// debugging
// useEffect(() => {
@@ -52,7 +63,7 @@ export default function Message({
}
}, [last, message]);
const enterEdit = cancel => setCurrentEditId(cancel ? -1 : message.messageId);
const enterEdit = (cancel) => setCurrentEditId(cancel ? -1 : message.messageId);
const handleWheel = () => {
if (blinker) {
@@ -62,6 +73,24 @@ export default function Message({
}
};
const getError = (text) => {
const errorMessage = text.length > 512 ? text.slice(0, 512) + '...' : text;
const match = text.match(/\{[^{}]*\}/);
var json = match ? match[0] : '';
if (isJson(json)) {
json = JSON.parse(json);
if (json.code === 'invalid_api_key') {
return 'Invalid API key. Please check your API key and try again. You can access your API key by clicking on the model logo in the top-left corner of the textbox.';
} else if (json.type === 'insufficient_quota') {
return "We're sorry, but the default API key has reached its limit. To continue using this service, please set up your own API key. You can do this by clicking on the model logo in the top-left corner of the textbox.";
} else {
return `Oops! Something went wrong. Please try again in a few moments. Here's the specific error message we encountered: ${errorMessage}`;
}
} else {
return `Oops! Something went wrong. Please try again in a few moments. Here's the specific error message we encountered: ${errorMessage}`;
}
};
const props = {
className:
'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 bg-white dark:text-gray-100 group dark:bg-gray-800'
@@ -109,17 +138,14 @@ export default function Message({
const clickSearchResult = async () => {
if (!searchResult) return;
getConversationQuery.refetch(message.conversationId).then(response => {
getConversationQuery.refetch(message.conversationId).then((response) => {
switchToConversation(response.data);
});
};
return (
<>
<div
{...props}
onWheel={handleWheel}
>
<div {...props} onWheel={handleWheel}>
<div className="relative m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
<div className="relative flex h-[30px] w-[30px] flex-col items-end text-right text-xs md:text-sm">
{typeof icon === 'string' && icon.match(/[^\\x00-\\x7F]+/) ? (
@@ -149,7 +175,7 @@ export default function Message({
{error ? (
<div className="flex flex min-h-[20px] flex-grow flex-col items-start gap-2 gap-4 text-red-500">
<div className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-100">
{`An error occurred. Please try again in a few moments.\n\nError message: ${text}`}
{getError(text)}
</div>
</div>
) : edit ? (
@@ -171,10 +197,7 @@ export default function Message({
>
Save & Submit
</button>
<button
className="btn btn-neutral relative"
onClick={() => enterEdit(true)}
>
<button className="btn btn-neutral relative" onClick={() => enterEdit(true)}>
Cancel
</button>
</div>

View File

@@ -2,20 +2,9 @@ import React, { useState } from 'react';
import { useRecoilValue } from 'recoil';
import EndpointOptionsDialog from '../Endpoints/EndpointOptionsDialog';
import { cn } from '~/utils/';
import { Button } from '../ui/Button.tsx';
import store from '~/store';
const clipPromptPrefix = str => {
if (typeof str !== 'string') {
return null;
} else if (str.length > 10) {
return str.slice(0, 10) + '...';
} else {
return str;
}
};
const MessageHeader = ({ isSearchView = false }) => {
const [saveAsDialogShow, setSaveAsDialogShow] = useState(false);
const conversation = useRecoilValue(store.conversation);
@@ -56,12 +45,14 @@ const MessageHeader = ({ isSearchView = false }) => {
<>
<div
className={cn(
'dark:text-gray-450 w-full gap-1 border-b border-black/10 bg-gray-50 text-sm text-gray-500 transition-all hover:bg-gray-100 hover:bg-opacity-30 dark:border-gray-900/50 dark:bg-gray-700 dark:hover:bg-gray-600 dark:hover:bg-opacity-100',
'dark:text-gray-450 w-full gap-1 border-b border-black/10 bg-gray-50 text-sm text-gray-500 transition-all hover:bg-gray-100 hover:bg-opacity-30 dark:border-gray-900/50 dark:bg-gray-700 dark:hover:bg-gray-600 dark:hover:bg-opacity-100 dark:text-gray-500',
endpoint === 'chatGPTBrowser' ? '' : 'cursor-pointer '
)}
onClick={() => (endpoint === 'chatGPTBrowser' ? null : setSaveAsDialogShow(true))}
>
<div className="d-block flex w-full items-center justify-center p-3">{getConversationTitle()}</div>
<div className="d-block flex w-full items-center justify-center p-3">
{getConversationTitle()}
</div>
</div>
<EndpointOptionsDialog

View File

@@ -17,7 +17,7 @@ export default function MultiMessage({
const [siblingIdx, setSiblingIdx] = useRecoilState(store.messagesSiblingIdxFamily(messageId));
const setSiblingIdxRev = value => {
const setSiblingIdxRev = (value) => {
setSiblingIdx(messagesTree?.length - value - 1);
};
@@ -41,19 +41,19 @@ export default function MultiMessage({
return (
<>
{messagesTree
? messagesTree.map(message => (
<Message
key={message.messageId}
conversation={conversation}
message={message}
scrollToBottom={scrollToBottom}
currentEditId={currentEditId}
setCurrentEditId={null}
siblingIdx={1}
siblingCount={1}
setSiblingIdx={null}
/>
))
? messagesTree.map((message) => (
<Message
key={message.messageId}
conversation={conversation}
message={message}
scrollToBottom={scrollToBottom}
currentEditId={currentEditId}
setCurrentEditId={null}
siblingIdx={1}
siblingCount={1}
setSiblingIdx={null}
/>
))
: null}
</>
);

View File

@@ -1,11 +1,10 @@
import React from 'react';
export default function ScrollToBottom({ scrollHandler}) {
export default function ScrollToBottom({ scrollHandler }) {
return (
<button
onClick={scrollHandler}
className="absolute right-6 bottom-[124px] z-10 cursor-pointer rounded-full border border-gray-200 bg-gray-50 text-gray-600 dark:border-white/10 dark:bg-white/10 dark:text-gray-200 md:bottom-[120px]"
className="absolute bottom-[124px] right-6 z-10 cursor-pointer rounded-full border border-gray-200 bg-gray-50 text-gray-600 dark:border-white/10 dark:bg-white/10 dark:text-gray-200 md:bottom-[120px]"
>
<svg
stroke="currentColor"
@@ -19,12 +18,7 @@ export default function ScrollToBottom({ scrollHandler}) {
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="12"
y1="5"
x2="12"
y2="19"
/>
<line x1="12" y1="5" x2="12" y2="19" />
<polyline points="19 12 12 19 5 12" />
</svg>
</button>

View File

@@ -1,26 +1,58 @@
import React from 'react';
export default function SiblingSwitch({
siblingIdx,
siblingCount,
setSiblingIdx
}) {
export default function SiblingSwitch({ siblingIdx, siblingCount, setSiblingIdx }) {
const previous = () => {
setSiblingIdx(siblingIdx - 1);
}
};
const next = () => {
setSiblingIdx(siblingIdx + 1);
}
};
return siblingCount > 1 ? (
<>
<button className="dark:text-white disabled:text-gray-300 dark:disabled:text-gray-400" onClick={previous} disabled={siblingIdx==0}>
<svg stroke="currentColor" fill="none" strokeWidth="1.5" viewBox="0 0 24 24" strokeLinecap="round" strokeLinejoin="round" className="h-3 w-3" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><polyline points="15 18 9 12 15 6"></polyline></svg>
<button
className="disabled:text-gray-300 dark:text-white dark:disabled:text-gray-400"
onClick={previous}
disabled={siblingIdx == 0}
>
<svg
stroke="currentColor"
fill="none"
strokeWidth="1.5"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-3 w-3"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
<span className="flex-grow flex-shrink-0">{siblingIdx + 1}/{siblingCount}</span>
<button className="dark:text-white disabled:text-gray-300 dark:disabled:text-gray-400" onClick={next} disabled={siblingIdx==siblingCount-1}>
<svg stroke="currentColor" fill="none" strokeWidth="1.5" viewBox="0 0 24 24" strokeLinecap="round" strokeLinejoin="round" className="h-3 w-3" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><polyline points="9 18 15 12 9 6"></polyline></svg>
<span className="flex-shrink-0 flex-grow">
{siblingIdx + 1}/{siblingCount}
</span>
<button
className="disabled:text-gray-300 dark:text-white dark:disabled:text-gray-400"
onClick={next}
disabled={siblingIdx == siblingCount - 1}
>
<svg
stroke="currentColor"
fill="none"
strokeWidth="1.5"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-3 w-3"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
</>
):null;
</>
) : null;
}

View File

@@ -76,7 +76,7 @@ export default function Messages({ isSearchView = false }) {
timeoutId = setTimeout(handleScroll, 100);
};
const scrollHandler = e => {
const scrollHandler = (e) => {
e.preventDefault();
scrollToBottom();
};
@@ -87,10 +87,7 @@ export default function Messages({ isSearchView = false }) {
ref={scrollableRef}
onScroll={debouncedHandleScroll}
>
<div
className="dark:gpt-dark-gray mb-32 h-auto md:mb-48"
ref={screenshotTargetRef}
>
<div className="dark:gpt-dark-gray mb-32 h-auto md:mb-48" ref={screenshotTargetRef}>
<div className="dark:gpt-dark-gray flex h-auto flex-col items-center text-sm">
<MessageHeader isSearchView={isSearchView} />
{_messagesTree === null ? (

Some files were not shown because too many files have changed in this diff Show More