Compare commits

...

116 Commits

Author SHA1 Message Date
Danny Avila
47d0184990 npm all prod(deps): bump mdast-util-from-markdown from 1.3.0 to 1.3.1 (#447) (#453)
Bumps [mdast-util-from-markdown](https://github.com/syntax-tree/mdast-util-from-markdown) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/syntax-tree/mdast-util-from-markdown/releases)
- [Commits](https://github.com/syntax-tree/mdast-util-from-markdown/compare/1.3.0...1.3.1)

---
updated-dependencies:
- dependency-name: mdast-util-from-markdown
  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-06-05 12:15:53 -04:00
Danny Avila
ee59fa40f5 npm all prod(deps): bump @babel/plugin-transform-react-jsx (#446) (#452)
Bumps [@babel/plugin-transform-react-jsx](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-react-jsx) from 7.21.5 to 7.22.3.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.22.3/packages/babel-plugin-transform-react-jsx)

---
updated-dependencies:
- dependency-name: "@babel/plugin-transform-react-jsx"
  dependency-type: indirect
  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-06-05 12:10:41 -04:00
Danny Avila
68b3731d06 npm all prod(deps): bump micromark-extension-gfm-task-list-item (#445) (#451)
Bumps [micromark-extension-gfm-task-list-item](https://github.com/micromark/micromark-extension-gfm-task-list-item) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/micromark/micromark-extension-gfm-task-list-item/releases)
- [Commits](https://github.com/micromark/micromark-extension-gfm-task-list-item/compare/1.0.4...1.0.5)

---
updated-dependencies:
- dependency-name: micromark-extension-gfm-task-list-item
  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-06-05 12:05:58 -04:00
Danny Avila
e4889ff8bb npm all prod(deps): bump postcss-double-position-gradients (#444) (#450)
Bumps [postcss-double-position-gradients](https://github.com/csstools/postcss-plugins/tree/HEAD/plugins/postcss-double-position-gradients) from 4.0.3 to 4.0.4.
- [Changelog](https://github.com/csstools/postcss-plugins/blob/main/plugins/postcss-double-position-gradients/CHANGELOG.md)
- [Commits](https://github.com/csstools/postcss-plugins/commits/HEAD/plugins/postcss-double-position-gradients)

---
updated-dependencies:
- dependency-name: postcss-double-position-gradients
  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-06-05 12:01:23 -04:00
Danny Avila
2c026d11a5 npm all prod(deps): bump @babel/helper-member-expression-to-functions (#443) (#449)
Bumps [@babel/helper-member-expression-to-functions](https://github.com/babel/babel/tree/HEAD/packages/babel-helper-member-expression-to-functions) from 7.21.5 to 7.22.3.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.22.3/packages/babel-helper-member-expression-to-functions)

---
updated-dependencies:
- dependency-name: "@babel/helper-member-expression-to-functions"
  dependency-type: indirect
  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-06-05 11:55:15 -04:00
Danny Avila
1a252170f5 chore(package): bump meilisearch (#448)
* npm api prod(deps): bump meilisearch from 0.32.5 to 0.33.0 in /api (#436)

Bumps [meilisearch](https://github.com/meilisearch/meilisearch-js) from 0.32.5 to 0.33.0.
- [Release notes](https://github.com/meilisearch/meilisearch-js/releases)
- [Commits](https://github.com/meilisearch/meilisearch-js/compare/v0.32.5...v0.33.0)

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

* chore(package): update meilisearch package from 0.32.3 to 0.33.0
chore(package): update cross-fetch package from 3.1.5 to 3.1.6 in meilisearch package dependencies

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-05 11:48:05 -04:00
Danny Avila
d40aaa703d chore: change dependabot settings for all packages (#442) 2023-06-05 11:22:14 -04:00
Danny Avila
44005258fc chore: create develop branch and change dependabot settings (#435) 2023-06-05 10:35:04 -04:00
Danny Avila
19495a461d chore(.gitignore): add .env.test to gitignore (#424)
feat(api): update @waylaidwanderer/chatgpt-api to version 1.37.0
2023-06-03 08:23:11 -04:00
Danny Avila
fcf068dddf style(NewConversationMenu): change dropdown menu background color to dark gray (#419)
style(EndpointItem): change active background color to light gray
2023-06-02 00:35:43 -04:00
Anirudh
7468b3011f Added Settings Modal (#342)
* Improve UI with style changes and add Settings button

- Improved the UI of the `Input` and `Message` components.
- Added a `Settings` button to the `NavLinks` component.
- Introduced a `Settings` component to handle user settings.
- Refactored the `Dialog` component for consistency.

* Revert not needed changes

* Updated style.css to only work for select

* feat: Remove Dark Mode component and add theme selection feature

This commit removes the Dark Mode component from the navigation bar and replaces it with a theme selection dropdown menu in the Settings dialog. The implementation of the theme selection feature includes a function that allows the user to set the theme based on the system, light, or dark mode.

* Add auto theme setting to Settings component.

This commit adds a new state variable to keep track of whether the auto theme is enabled or not. It also registers an event listener to update the theme based on system preference changes. The event listener is removed when the component is unmounted.

* Improve user experience by allowing customized themes
- Create `selectedOption` state to track user-selected theme
- Remove unused `isAutoTheme` state variable

* feat(Nav): Add SVG icon to settings gear

This commit adds an SVG icon to the settings gear in the Navigation component's Settings file. The new SVG icon replaces the previous GearIcon component.

* refactor(ui): Update overlay background color

This commit updates the background color of the overlay in the AlertDialog and Dialog components by changing the classes applied to the elements. The new color is a transition from `bg-black/50 backdrop-blur-sm` to `bg-gray-500/90 dark:bg-gray-800/90`. This change improves the readability of the dialog boxes.

* Refactor ThemeContext to include system theme and fix bug in Settings

The ThemeContext now includes a "system" theme and ClearConvos no longer relies on the "selected option" state to update the theme. The bug is now fixed if the system theme changes.

* Refactor DialogTemplate styles and color scheme

Adjusted the color scheme of the DialogTemplate component to dark mode, updated the background color to gray-900 and removed unnecessary classes.

* Refactor: Change button logic to require confirmation before clearing convos

This commit refactors the code by adding a confirmation dialog to prompt for a user's confirmation before clearing all conversations in the Settings.jsx file. The change ensures the user is aware of the irreversible action before initiating the clearConvos function. Additionally, the commit updates the clear chat button's class name and changes the button's onClick logic to call the confirmClearConvos function instead of directly invoking the clearConvos method.

* Refactor component name to reflect functionality change.

- Changed component name from ClearConvos to Settings to support potential future use cases.

* Refactor conversation clearing functionality in `Settings.jsx`

This commit optimizes the conversation clearing functionality in the `Settings.jsx` component by removing the `confirmClearConvos` function and directly calling the `clearConvos` function on confirmation. This change will simplify the code and improve the user experience.

* Refactor Input component UI styles

Simplify Input component styles by simplifying the gradient background, removing border color styles, and updating button styles.

* feat: Add e2e test for Settings modal

This commit adds an e2e test to verify whether the Settings modal is displayed on the landing page. It uses a headless browser to navigate to the page and interacts with it to verify if the dialog and its components are visible.

* test: Add Navigation and Settings tests

Add Navigation and Settings tests to verify that the navigation bar and Settings button are visible and that the Settings modal displays the expected content. The settings modal verification includes checking whether the modal is visible, if the modal title, tab list, clear conversation button and theme are present, and if the theme option can be selected to change the mode.

* Quick fix

* feat(navbar): Add confirmation before clearing conversations

Adding confirmation modal to prevent accidentally clearing conversations. Before, once you clicked on the "Clear" button it immediately clears all conversations. With this change, if you click on "Clear" the first time, it will change the text to "Confirm Clear" and if you click it again, it will clear all conversations.

* Add click functionality to the navigation bar and improve UI design

The code introduced click functionality to the nav bar and improved the user interface. It also used the new theme select feature to change the theme to dark.

* test: Add test for dark mode theme change

Refactor the test for Navigation suite to check for the 'dark' class in the HTML element when the 'dark' theme is selected in the modal. This ensures that the dark mode theme change works correctly, and improves test coverage.

* Improve navigation test clarity

This commit improves code clarity and adds more detailed test assertions to the navigation suite. New assert statements are added to check whether the modal theme selection changes the theme and that the HTML element receives the 'dark' class. A new function `changeMode` was introduced to avoid code repetition. A short description was added to the commit message to adhere to best practices.

* Improve navigation test clarity

This commit improves code clarity and adds more detailed test assertions to the navigation suite. New assert statements are added to check whether the modal theme selection changes the theme and that the HTML element receives the 'dark' class. A new function `changeMode` was introduced to avoid code repetition. A short description was added to the commit message to adhere to best practices.

* Hotfix

* Removed repetation

* Refactor: Change text-gray-400 to text-white/50 to make tailwind more cleaner

* style: Update CSS classes to improve the conversation UI

- Update Conversation component to improve UX
- Changed styling for group hover effect using shades of gray
- Improved color contrast of the Message component for easy readability
- Replaced class names in buildTree.js with a new class name
- Added a new color theme (gray-1000) in tailwind.config to replace an old background color.

* Refactor EndpointItem, EndpointItems, and NewConversationMenu for better user experience

- The `EndpointItem` component now accepts an `isSelected` prop instead of `onSelect` to better reflect its usage in `EndpointItems` and `NewConversationMenu`.
- `EndpointItems` component now has a `selectedEndpoint` prop to highlight the selected item in the list.
- `NewConversationMenu` now has a gap between the endpoint options to improve user experience.

* Added error messages

* refactor: Improve endpoint menu highlighting and error handling

In the UI, when the user selects an endpoint, the active class is now properly set. In the error handling function, `isJson` is now a private function called by `getError`, which provides better parsing of error messages, and returns more succinct messages upon encountering specific errors. Finally, a new end-to-end test has been added to check if the active class is properly set on selecting an endpoint in the new conversation menu.

* test: Add Conversation and Change Path of Auth JSON

In the Landing spec, test the functionality to create conversations and check that the number of items has increased. In the Popup spec, change the path of the Auth JSON used by the context.

* Fixed logo issues

* Make everything not rounded

* Added time

---------

Co-authored-by: Danny Avila <110412045+danny-avila@users.noreply.github.com>
2023-06-02 00:32:35 -04:00
Anirudh
dade7b450f feat: Add clear button to search bar (#328)
* feat: Add clear button to search bar

This commit introduces a clear button to the SearchBar component using the X icon from Lucide-React. When the user enters a query in the input field, the clear button appears allowing them to easily remove the search term. The clear button is hidden when there is no search term entered.

* Refactor SearchBar component to improve user experience

Changed SearchBar's input field to add padding on the left side and an absolute positioned search icon. Also, added absolute positioned X icon on the right side when there is an input value, ensuring a better user experience.

* Refactor SearchBar component to show Clear Search icon dynamically

This commit makes changes to the SearchBar React component to render the Clear Search X icon only when the input field has a value. A showClearIcon state using useState hook is added and updated every time the input value changes. The useEffect hook is used to handle the case when the user clears the input value. This allows better UX by providing clear intent to the user that the icon is clickable and will clear the search query.

* Improve UX: Add styling to clear button & export button

This commit modifies the NavLinks component to improve user experience by removing a rounded styling to the "Clear conversations" and "Export conversations" buttons. Prior to this change, the buttons had a rounded styling.

* Refactor submit button styling for improved accessibility and readability.

Changed submit button styling for better accessibility and readability, including adjustments to padding and hover effects. The new styles ensure that the button is easily clickable for all users, while also improving its visual appearance.

* hotfix

* Improve UI styling in Conversation component

Changed the background color and hover effect of the conversation link in Conversation component to make it more visually appealing. The previous background color was '#2A2B32' and now it's 'gray-800'. The 'px-4' class has also been changed to 'hover:pr-4' for better readability.

---------

Co-authored-by: Danny Avila <110412045+danny-avila@users.noreply.github.com>
2023-06-02 00:11:34 -04:00
Danny Avila
7fbf27c5aa chore(.gitignore): add client/public/images/ to ignore list (#417)
refactor(chatgpt-client.js): free encoder memory after use
feat(chatgpt-client.tokens.js): add script to test memory usage of ChatGPTClient
2023-06-02 00:08:19 -04:00
Fuegovic
4705975e59 feat:add hyperlink to bing.com in SetTokenDialog (#414) 2023-05-31 00:41:01 -04:00
Danny Avila
2f59c82bec chore(api): update chatgpt-api package version to 1.36.3 (#404)
docs(api): update BINGAI_TOKEN instructions in .env.example
docs(client): update BINGAI_TOKEN instructions in SetTokenDialog component
2023-05-29 11:00:51 -04:00
Fuegovic
6a34978e98 Fix: typo and phrasing (#393)
* Update FEATURE-REQUEST.yml

Fix typo and phrasing

* Update pull_request_template.md

add one option to type of change
2023-05-28 17:55:57 -04:00
Fuegovic
d437e4b8cd update: "documents" folder to "docs" (#391)
* Rename .github/PULL_REQUEST_TEMPLATE/PULL-REQUEST.md to .github/pull_request_template.md

fix: Pull Request Template Location

* documents -> docs

* Update windows_install.md

Fix: Docker hyperlink

* Update linux_install.md

Fix: Layout (step 6)

* Rename docs/contributions/code_of_conduct.md to CODE_OF_CONDUCT.md

fix: Code of Conduct location according to GitHub's Guide

* Update CODE_OF_CONDUCT.md

Update: Contact info

* Update README.md

Update: Code of Conduct hyperlink in TOC

* Update CODE_OF_CONDUCT.md

Update: Link to ReadMe

* Update CONTRIBUTORS.md

update: add new name to the list

* Update and rename docs/contributions/contributor_guidelines.md to CONTRIBUTING.md

fix: change location according to GitHub's standards

* Delete CONTRIBUTORS.md

delete: contributor.md from root (already present in readme)

* Update SECURITY.md

* Update CONTRIBUTING.md

Update discord link to point to rules

* Update README.md

Update discord link to point to rules

* Update README.md

fix: ToC
2023-05-27 07:03:28 -04:00
Fuegovic
f40a2f8ee8 update: documentation (#389)
* Update docker_install.md

update Bing Token instructions

* Update linux_install.md

Update Bing Token Instructions
Add # markers to sections

* Update mac_install.md

Update Bing Token Instructions
Fix Formating
Recommend Docker

* Update windows_install.md

Update Bing Token Instructions

* Update linux_install.md

Recommend Docker

* Create QUESTION.yml

Questions Template

* Update QUESTION.yml

fix syntax

* Update QUESTION.yml

* Update QUESTION.yml

* Create FEATURE-REQUEST

* Rename FEATURE-REQUEST to FEATURE-REQUEST.yml

add file extension
2023-05-26 22:22:11 -04:00
Danny Avila
2d31c9f8b6 chore: bump package versions to 0.4.7 (#388) 2023-05-26 17:56:23 -04:00
Danny Avila
fd5afc09a2 chore(tests): add e2e tests for messaging suite (#387)
* feat(NewConversationMenu): add id to the new conversation menu button
refactor(EndpointItem): remove onSelect prop and setTokenDialogOpen state variable
test(messages.spec.js): add e2e test for messaging suite to check if textbox is focused after receiving message

* test(Input): add test id to input field for e2e testing
test(messages.spec.js): add endpoint variable and refactor test to check if textbox is focused after receiving message

* test(messages.spec.js): refactor test to use a variable for message content

Refactored the test to use a variable for message content instead of a hardcoded string.
2023-05-26 17:34:08 -04:00
Danny Avila
c0845ad0b1 Fix Input losing focus (#382)
* fix(PaLM2): input losing focus on message stream ending

* fix(askOpenAI.js): fix typo in variable name from newUserMassageId to newUserMessageId

* feat(chatgpt-browser.js, askBingAI.js, askChatGPTBrowser.js): add onEventMessage callback to browserClient

Add onEventMessage callback to browserClient to handle event messages from the server. In askChatGPTBrowser.js, add a getPartialMessage variable to store the partial message text. In askBingAI.js, fix a typo in the variable name newUserMassageId to newUserMessageId. In askChatGPTBrowser.js, remove the preSendRequest parameter and move the sendMessage call to the onEventMessage callback. In askChatGPTBrowser.js, add a check for null or undefined value of getPartialMessage before appending it to the error message.

* fix(bing): input no longer loses input focus as convoId is persisted from beginning of convo

* refactor(Input): remove unused code and fix input autofocus
feat(package.json): add e2e:test-auth script to test authentication flow with saved storage
2023-05-26 14:32:13 -04:00
Danny Avila
11b98d3d13 refactor(chatgpt-client.js): initialize usage object with empty object instead of null (#386)
refactor(chatgpt-client.js): simplify usage object assignment
2023-05-26 09:43:35 -04:00
Danny Avila
4f17e69f1b fix(tokenizer): error handle encoding for invalid encoding data (#385) 2023-05-26 09:40:08 -04:00
Danny Avila
b912e7a3dd Update BUG-REPORT.yml 2023-05-26 09:02:32 -04:00
Danny Avila
743a9315ff Update BUG-REPORT.yml 2023-05-26 09:00:33 -04:00
Danny Avila
ea2135a237 chore(api): remove unused crypto dependency from package.json (#381) 2023-05-25 14:54:46 -04:00
Dan Orlando
6a1983bc6c refactor: remove bcrypt (#375) 2023-05-25 14:54:24 -04:00
Fuegovic
07796d9e48 Update BUG-REPORT.yml (#379)
remove other tags than "bug" from the bug report
2023-05-25 13:17:03 -04:00
Danny Avila
634849ec12 fix(Bing): Use full cookies string instead of just _U cookie (#369) 2023-05-23 13:58:18 -04:00
Danny Avila
112c6c5b19 fix (PaLM2): messages will properly regenerate (#368)
* making progress to fix regen for PaLM

* fix (PaLM2): messages will properly regenerate
2023-05-23 06:55:23 -04:00
Olivier Contant
b8c3ae5e8f Update SECURITY.md (#367)
Include OWASP reference to Vulnerability Disclosure process.
2023-05-23 06:41:01 -04:00
Danny Avila
07fa0f39fd Fix (PaLM2): Persist PaLM presets after initial message (#366)
* refactor(askGoogle.js): extract saveConvo function call to a separate function
feat(askGoogle.js): add endpoint property to the conversation object
refactor(handleSubmit.js): rename chatGptLabel to modelLabel in useMessageHandler function

* refactor(askGoogle.js): remove unused endpointOption spread operator
2023-05-22 20:50:10 -04:00
Dan Orlando
4eda4542b7 feat: Setup Unit Test Environment and Refactor Typescript Config (#365)
* modify tsconfig and set up unit tests

* generate .d.ts files

* setup project dependencies and configuration for unit tests

* Add test setup and layout-test-utils along with first spec

* Add paths back to tsconfig

* remove type=module from package.json

* Add typescript definition for .env

* update package-lock
2023-05-22 20:49:48 -04:00
dependabot[bot]
dbfef342e2 npm all prod(deps): bump fast-redact from 3.1.2 to 3.2.0 (#360)
Bumps [fast-redact](https://github.com/davidmarkclements/fast-redact) from 3.1.2 to 3.2.0.
- [Release notes](https://github.com/davidmarkclements/fast-redact/releases)
- [Commits](https://github.com/davidmarkclements/fast-redact/compare/v3.1.2...v3.2.0)

---
updated-dependencies:
- dependency-name: fast-redact
  dependency-type: indirect
  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-22 13:28:19 -04:00
Danny Avila
735eb159db npm client dev(deps-dev): bump @vitejs/plugin-react from 3.1.0 to 4.0.0 (#364) 2023-05-22 13:23:17 -04:00
Danny Avila
bf911074cf npm client prod(deps): bump @types/node from 18.16.14 to 20.2.3 (#363) 2023-05-22 12:33:52 -04:00
dependabot[bot]
7ec061c694 npm all prod(deps): bump @csstools/postcss-oklab-function (#356)
Bumps [@csstools/postcss-oklab-function](https://github.com/csstools/postcss-plugins/tree/HEAD/plugins/postcss-oklab-function) from 2.2.1 to 2.2.2.
- [Changelog](https://github.com/csstools/postcss-plugins/blob/main/plugins/postcss-oklab-function/CHANGELOG.md)
- [Commits](https://github.com/csstools/postcss-plugins/commits/HEAD/plugins/postcss-oklab-function)

---
updated-dependencies:
- dependency-name: "@csstools/postcss-oklab-function"
  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-22 12:21:55 -04:00
Danny Avila
a6f3eb4c0d npm client prod(deps): bump esbuild from 0.17.15 to 0.17.19 (#361) 2023-05-22 12:17:34 -04:00
dependabot[bot]
dc5f9d8474 npm all prod(deps): bump eslint from 8.40.0 to 8.41.0 (#354)
Bumps [eslint](https://github.com/eslint/eslint) from 8.40.0 to 8.41.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.40.0...v8.41.0)

---
updated-dependencies:
- dependency-name: eslint
  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-22 12:10:50 -04:00
dependabot[bot]
c1349fbfaa npm all prod(deps): bump tar from 6.1.14 to 6.1.15 (#353)
Bumps [tar](https://github.com/isaacs/node-tar) from 6.1.14 to 6.1.15.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v6.1.14...v6.1.15)

---
updated-dependencies:
- dependency-name: tar
  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-22 12:04:41 -04:00
Olivier Contant
6f9da5f7df Update coding_conventions.md (#350)
Fixed typo missing letter.
2023-05-22 00:09:33 -04:00
Danny Avila
10de50416b feat(HoverButtons.jsx): enable message regeneration for bingAI endpoint (#349)
feat(HoverButtons.jsx): add active class to copy button only if message is not created by user
2023-05-21 14:01:46 -04:00
Danny Avila
4beb06aa4b Minor fixes: tokenizer, default Bing toneStyle, SiblingSwitch (#348)
* fix: tokenizer will count completion tokens correctly, remove global var, will allow unofficial models for alternative endpoints

* refactor(askBingAI.js, Settings.jsx, types.ts, cleanupPreset.js, getDefaultConversation.js, handleSubmit.js): change default toneStyle to 'creative' instead of 'fast' for Bing AI endpoint.

* fix(SiblingSwitch): correctly appears now
style(HoverButtons.jsx): add 'active' class to hover buttons
2023-05-21 12:43:06 -04:00
Danny Avila
791b515937 Cleanup root dir, move dev-related files into /documents/ (#347)
* chore: cleanup root dir and move extraneous dev related files to documents/dev

* chore: cleanup root dir and move extraneous dev related files to documents/dev
2023-05-21 08:56:06 -04:00
Fuegovic
8d4ef16b7f docs : update the documentation (#345)
* Add files via upload

* Delete documents/report_templates directory

* Update PR-TEMPLATE.md

* Update README.md

removed templates from TOC

* Update SECURITY.md

- update to follow documentation guidelines
- update discord link to point to issues

* Update SECURITY.md

* Update README.md

add security to TOC

* Delete pull_request_template.md

moved to .github

* Rename PR-TEMPLATE.md to PULL-REQUEST.md

* Update mac_install.md

clean up and update

* Update windows_install.md

fix formating and change update instructions

* Update windows_install.md

add docker recommendation

* Update windows_install.md

* Update mac_install.md
2023-05-21 08:42:16 -04: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
Danny Avila
dbf45196ee Release 0.4.5 (#282)
* Release 0.4.5

* Update @waylaidwanderer/node-chatgpt-api to latest version
* Update dockerfiles to use workspaces and ensure packages are @ latest
* Remove package-lock.json files from workspace directories as no longer needed

* refactor(api): remove deprecated text-davinci-002-render-paid model from CHATGPT_MODELS
refactor(api/client): change model comparison to use startsWith() instead of === for GPT-4 models
2023-05-16 14:30:24 -04:00
Fuegovic
45a2aaf7b8 docs : add basic info document in multiple languages (#285)
* Create multilingual_information.md

add a multilingual document with basic information about the project for non-native English speakers

* Update README toc to add multilingual info

add the multilingual info doc to the table of content (under General Information)
2023-05-16 13:30:52 -04:00
Dan Orlando
c02c62f3b1 fix: fix link to coding conventions doc in contributor guidelines (#283)
* doc: coding conventions and proposal submissions

* make coding_contention.md path relative in contributor guidelines

* fix: remove / from coding conventions link
2023-05-16 12:09:17 -04:00
Dan Orlando
4718674688 doc: coding conventions and proposal submissions (#250)
* doc: coding conventions and proposal submissions

* doc: add code standards to TOC

* make coding_contention.md path relative in contributor guidelines
2023-05-16 09:50:16 -04:00
Danny Avila
0e3c115368 Update README.md 2023-05-16 09:48:54 -04:00
Danny Avila
cc506c23af Update README.md 2023-05-16 06:53:56 -04:00
Danny Avila
1f77d94b7e Update README.md 2023-05-16 06:52:47 -04:00
Danny Avila
5711ff27ee fix(getIcon.jsx): match initial styling better with official (#277) 2023-05-15 12:15:33 -04:00
David Shin
3120602d6a feat: Add user icon in messages (#275)
* Update GPT4 model icon

* Add user icon support in messages
2023-05-15 11:51:58 -04:00
David Shin
9f36e195bc Update GPT4 model icon (#274) 2023-05-15 10:08:30 -04:00
Fuegovic
9de7da91a7 Fix: install instructions (#272)
* Update windows_install.md

removed -dev argument

* Update mac_install.md

removed `-dev` arguments

* Update linux_install.md

removed "-dev" argument

* Update windows_install.md

correction to update procedure

* Update windows_install.md

updat bat file instruction

* Update mac_install.md

update bash command

* Update linux_install.md

update bash script and update instructions

* Update linux_install.md

fix mistake in update instruction
2023-05-15 07:49:49 -04:00
Danny Avila
501a15a18f Release 0.4.4 (#271) 2023-05-14 20:39:40 -04:00
Danny Avila
6049c9e3ff Fix react errors, max context tokens, and preset mobile view (#269)
* fix: react errors

* fix: max tokens issue

* fix: max tokens issue
2023-05-14 17:26:21 -04:00
Pawan Kumar
262b402606 fix code to adjust max_tokens according to model selection (#263) 2023-05-14 12:16:38 -04:00
Danny Avila
56ea9563b8 refactor(style.css): change font file paths (#268) 2023-05-14 12:12:56 -04:00
Anirudh
2cd6612620 Fonts (#261) 2023-05-14 12:06:53 -04:00
Danny Avila
5d40396fb2 refactor(Conversation.js): change default pageSize from 12 to 14 in getConvosByPage and getConvosQueried functions. Remove unnecessary parentheses and curly braces in getConvosQueried function. Remove unnecessary parentheses in deleteConvos function. (#267) 2023-05-14 11:45:18 -04:00
Anirudh
93dd1eb036 Add Popup Menu to Save Space in Sidebar (#260)
---------

Co-authored-by: Danny Avila <110412045+danny-avila@users.noreply.github.com>
2023-05-14 11:42:17 -04:00
Anton Volnuhin
542a46dc7c Correct the typo in auth.json for accessing Google Palm (#266)
Co-authored-by: Anton Volnuhin <anton@volnuhin.ru>
2023-05-14 11:25:22 -04:00
Anirudh
bf31b1fea0 Msg Clipboard to checkmark (optimistic UX) (#247)
* revert unintended package-lock.json change

* used default checkmark which is included in project

---------

Co-authored-by: Danny Avila <110412045+danny-avila@users.noreply.github.com>
2023-05-14 09:00:20 -04:00
Danny Avila
25d4529ff9 Release v0.4.3 2023-05-13 17:10:19 -04:00
Danny Avila
33d7c67c04 Release v0.4.3 2023-05-13 17:09:25 -04:00
Danny Avila
dc8f762bac Release v0.4.3 2023-05-13 17:08:28 -04:00
Danny Avila
49041e16c7 chore: bump package versions to 0.4.3 (#265) 2023-05-13 16:59:45 -04:00
Danny Avila
3414690e42 Feat: PaLM 2 (#262)
* feat(api): add googleapis package to package.json
feat(api): add reqDemo.js file to make a request to Google Cloud AI Platform API to get a response from a chatbot model.

* feat: add PaLM2 support

* feat(conversationPreset.js): add support for topP and topK for google endpoint
feat(askGoogle.js): add support for topP and topK for google endpoint
feat(ask/index.js): add google endpoint
feat(endpoints.js): add google endpoint
feat(MessageHeader.jsx): add support for modelLabel for google endpoint
feat(PresetItem.jsx): add support for modelLabel for google endpoint
feat(HoverButtons.jsx): add support for google endpoint
feat(createPayload.ts): add google endpoint
feat(types.ts): add google endpoint
feat(store/endpoints.js): add google endpoint
feat(cleanupPreset.js): add support for topP and topK for google endpoint
feat(getDefaultConversation.js): add support for topP and topK for google endpoint
feat(handleSubmit.js): add support for topP and topK for google endpoint

* fix: messages payload

* refactor(GoogleClient.js): set maxContextTokens based on isTextModel value
feat(GoogleClient.js): add delay option to TextStream constructor
feat(getIcon.jsx): add support for google endpoint and PaLM2 model label

* feat: palm frontend changes

* feat(askGoogle.js): set default example to empty input and output
feat(Examples.jsx): add ability to add and remove examples
refactor(Settings.jsx): remove examples from props and setOption function

style(GoogleOptions): remove unnecessary whitespace after Settings2 import
feat(GoogleOptions): add addExample and removeExample functions to manage examples
fix(cleanupPreset): set default example to [{ input: '', output: ''}]
fix(getDefaultConversation): set default example to [{ input: '', output: ''}]
fix(handleSubmit): set default example to [{ input: '', output: ''}]

* style(client): adjust height of settings and examples components to 350px
fix(client): fix path to palm.png image in getIcon.jsx file

* style(EndpointOptionsPopover.jsx, Examples.jsx, Settings.jsx): improve button styles and update input placeholders

* feat (palm): finalize examples on the frontend

* feat(GoogleClient.js): filter out empty examples in options
feat(GoogleClient.js): add support for promptPrefix in buildPayload method
feat(GoogleClient.js): add support for examples in buildPayload method
feat(conversationPreset.js): add maxOutputTokens field to conversation preset schema
feat(presetSchema.js): add examples field to preset schema
feat(askGoogle.js): add support for examples and promptPrefix in endpointOption
feat(EditPresetDialog.jsx): add Examples component for Google endpoint
feat(EditPresetDialog.jsx): add button to show/hide Examples component
feat(EditPresetDialog.jsx): add functionality to add, remove, and edit examples in Examples component
feat(EndpointOptionsDialog.jsx): change endpoint name to PaLM for Google endpoint
feat(Settings.jsx): add maxHeight prop to limit height of Settings component in EditPresetDialog and EndpointOptionsDialog

fix(Settings.jsx): add examples prop to ChatGPTBrowser component
fix(EndpointItem.jsx): add alternate name for google endpoint
fix(MessageHeader.jsx): change title for google endpoint to PaLM
feat(endpoints.js): add google endpoint to endpointsConfig
fix(cleanupPreset.js): add missing comma in examples array

* chore: change endpoint order

* feat(PaLM 2): complete for testing

* fix(PaLM): handle blocked messages
2023-05-13 16:29:06 -04:00
LaraClara
95c97561ae chore: NPM Workspaces and scripts (#244)
* chore: NPM Workspaces and scripts
- Allows everything to be run in the root directory

* chore:Update package-lock after workspace change

* docs: Minor docs typo fix
- most people run in dev mode, ie vite runs the server, this defaults to that method
2023-05-12 09:40:14 -04:00
Danny Avila
8bb4d7d590 Release 0.4.2 2023-05-11 16:46:27 -04:00
Danny Avila
94ad31dce3 Release 0.4.2 (#242)
* release: 0.4.2

* docs: update changelog and readme for v0.4.2 release

* docs(README.md): add important information about new user system and env variables
2023-05-11 16:39:44 -04:00
Danny Avila
91e9b167b3 fix(docker): update .dockerignore to include client/.env file (#241)
fix(docker): add COPY command to copy client/.env file into the container before building
2023-05-11 15:59:43 -04:00
Dan Orlando
53ea3dd9fb Feature/logging system with pino and sanitization (#214) (#227)
* feat: Create structured data logging system with Pino

This commit creates a new feature that enables structured data logging using the Pino logging library. The structured data logging feature allows for more granular and customizable logging, making it easier to analyze and debug issues in the application.

The changes made in this commit include:
- Adding support for structured data logging using the Pino API
- Adding support to redact sensible data from logging output using pino
  redact.
- Pino integrate natively with fluentd, logstash, Docker Logging Drivers
  and other JSON based system.

* Add pino package to project

* Logging-System: Add support for an array of regex to redact

* Logging-Systems: Add Redact Patterns and Pino Redact Paths + Boolean logics wasn't right.

Co-authored-by: Olivier Contant <ocontant@users.noreply.github.com>
2023-05-10 23:59:26 -04:00
Danny Avila
c72a3a0362 refactor(titleConvo.js, endpoints.js): add support for AZURE_OPENAI_API_KEY environment variable (#235) 2023-05-10 23:56:24 -04:00
Danny Avila
bd068c9a5a feat(chatgpt-client.js, titleConvo.js, genAzureEndpoints.js): add support for Azure OpenAI API endpoint generation (#234)
This commit adds support for generating Azure OpenAI API endpoints in the
`chatgpt-client.js` and `titleConvo.js` files. The `genAzureEndpoint` function
in `genAzureEndpoints.js` generates the endpoint URL based on the provided
parameters. The `chatgpt-client.js` and `titleConvo.js` files now use this
function to generate the endpoint URL when the `AZURE_OPENAI_API_KEY` environment
variable is set.
2023-05-10 23:47:26 -04:00
Youth
7997c3137a * refactor(getCitations.js): add null check for adaptiveCards variable before accessing its properties (#232) 2023-05-10 21:23:15 -04:00
Fuegovic
b466b36e7a Update README.md to v0.4.1 (#224)
* Update CONTRIBUTORS.md

* Update CHANGELOG.md

v0.4.1 Changelog

v0.4.1 changelog and link
Contributors in the TOC

* Update README.md

add a link to the alternative docs from @DavidDev1334

* Update linux_install.md

Credit @DavidDev1334 for linux install doc
2023-05-09 22:12:12 -04:00
253 changed files with 29911 additions and 34016 deletions

View File

@@ -1,2 +1,3 @@
**/node_modules
**/.env
api/.env
client/dist/images

115
.eslintrc.js Normal file
View File

@@ -0,0 +1,115 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
commonjs: true,
es6: true
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
"plugin:jest/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', 'jest'],
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.
}
}
};

64
.github/ISSUE_TEMPLATE/BUG-REPORT.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Please give as many details as possible
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to Reproduce
description: Please list the steps needed to reproduce the issue.
placeholder: "1. Step 1\n2. Step 2\n3. Step 3"
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- Mobile (iOS)
- Mobile (Android)
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem. You can drag and drop, paste images directly here or link to them.
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/chatgpt-clone/blob/main/documents/contributions/code_of_conduct.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true

View File

@@ -0,0 +1,57 @@
name: Feature Request
description: File a feature request
title: "Enhancement: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill this out!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we contact you if we need more information?
placeholder: ex. email@example.com
validations:
required: false
- type: textarea
id: what
attributes:
label: What features would you like to see added?
description: Please provide as many details as possible.
placeholder: Please provide as many details as possible.
validations:
required: true
- type: textarea
id: details
attributes:
label: More details
description: Please provide additional details if needed.
placeholder: Please provide additional details if needed.
validations:
required: true
- type: dropdown
id: subject
attributes:
label: Which components are impacted by your request?
multiple: true
options:
- General
- UI
- Endpoints
- Plugins
- Other
- type: textarea
id: screenshots
attributes:
label: Pictures
description: If relevant, please include images to help clarify your request. You can drag and drop images directly here, paste them, or provide a link to them.
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/chatgpt-clone/blob/main/documents/contributions/code_of_conduct.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true

58
.github/ISSUE_TEMPLATE/QUESTION.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Question
description: Ask your question
title: "[Question]: "
labels: ["question"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill this!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: textarea
id: what-is-your-question
attributes:
label: What is your question?
description: Please give as many details as possible
placeholder: Please give as many details as possible
validations:
required: true
- type: textarea
id: more-details
attributes:
label: More Details
description: Please provide more details if needed.
placeholder: Please provide more details if needed.
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What is the main subject of your question?
multiple: true
options:
- Documentation
- Installation
- UI
- Endpoints
- User System/OAuth
- Other
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem. You can drag and drop, paste images directly here or link to them.
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/chatgpt-clone/blob/main/documents/contributions/code_of_conduct.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true

47
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
# 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
target-branch: "develop"
versioning-strategy: increase-if-necessary
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
target-branch: "develop"
versioning-strategy: increase-if-necessary
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
target-branch: "develop"
versioning-strategy: increase-if-necessary
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"

View File

@@ -1,40 +1,35 @@
Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change.
Fixes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
# How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
- [ ] Test A
- [ ] Test B
**Test Configuration**:
* Firmware version:
* Hardware:
* Toolchain:
* SDK:
# Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules
##
## [Go Back to ReadMe](../../README.md)
Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change.
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
- [ ] Documentation update
## How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration:
##
### **Test Configuration**:
##
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules

5
.gitignore vendored
View File

@@ -26,6 +26,7 @@ dist/
public/main.js
public/main.js.map
public/main.js.LICENSE.txt
client/public/images/
client/public/main.js
client/public/main.js.map
client/public/main.js.LICENSE.txt
@@ -48,6 +49,7 @@ bower_components/
# Environment
.npmrc
.env
.env.test
cache.json
api/data/
owner.yml
@@ -59,8 +61,9 @@ src/style - official.css
/playwright/.cache/
.DS_Store
*.code-workspace
.idea
# 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.js 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

@@ -1,5 +1,93 @@
# # Changelog
<details open>
<summary><strong>2023-05-14</strong></summary>
**Released [v0.4.4](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.4):**
1. The Msg Clipboard was changed to a checkmark for improved user experience by @techwithanirudh in PR [#247](https://github.com/danny-avila/chatgpt-clone/pull/247).
2. A typo in the auth.json path for accessing Google Palm was corrected by @antonme in PR [#266](https://github.com/danny-avila/chatgpt-clone/pull/266).
3. @techwithanirudh added a Popup Menu to save sidebar space in PR [#260](https://github.com/danny-avila/chatgpt-clone/pull/260).
4. The default pageSize in Conversation.js was increased from 12 to 14 by @danny-avila in PR [#267](https://github.com/danny-avila/chatgpt-clone/pull/267).
5. Fonts were updated by @techwithanirudh in PR [#261](https://github.com/danny-avila/chatgpt-clone/pull/261).
6. Font file paths in style.css were changed by @danny-avila in PR [#268](https://github.com/danny-avila/chatgpt-clone/pull/268).
7. Code was fixed to adjust max_tokens according to model selection by @p4w4n in PR [#263](https://github.com/danny-avila/chatgpt-clone/pull/263).
8. Various improvements were made, such as fixing react errors and adjusting the mobile view, by @danny-avila in PR [#269](https://github.com/danny-avila/chatgpt-clone/pull/269).
New contributors to the project include:
- @techwithanirudh, who made their first contribution in PR [#247](https://github.com/danny-avila/chatgpt-clone/pull/247).
- @antonme, who made their first contribution in PR [#266](https://github.com/danny-avila/chatgpt-clone/pull/266).
- @p4w4n, who made their first contribution in PR [#263](https://github.com/danny-avila/chatgpt-clone/pull/263).
The [full changelog can be found here](https://github.com/danny-avila/chatgpt-clone/compare/v0.4.3...v0.4.4)
</details>
<details>
<summary><strong>2023-05-13</strong></summary>
**Released [v0.4.3](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.3) which now supports Google's PaLM 2!**
![image](https://github.com/danny-avila/chatgpt-clone/assets/110412045/ec5e8ff3-6c3a-4f25-9687-d8558435d094)
**How to Setup PaLM 2 (via Google Cloud Vertex AI API)**
- Enable the Vertex AI API on Google Cloud:
- - https://console.cloud.google.com/vertex-ai
- Create a Service Account:
- - https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create?walkthrough_id=iam--create-service-account#step_index=1
- Make sure to click 'Create and Continue' to give at least the 'Vertex AI User' role.
- Create a JSON key, rename as 'auth.json' and save it in /api/data/.
**Alternatively**
- In your ./api/.env file, set PALM_KEY as "user_provided" to allow the user to provide a Service Account key JSON from the UI.
- They will follow the steps above except for renaming the file, simply importing the JSON when prompted.
- The key is sent to the server but never saved except in your local storage
**Note:**
- Vertex AI does not (yet) support response streaming for text generations, so response may seem to take long when generating a lot of text.
- Text streaming is simulated
You can check the full changelog in between v0.4.2 and v0.4.3 [here](https://github.com/danny-avila/chatgpt-clone/compare/v0.4.2...v0.4.3).
</details>
<details>
<summary><strong>2023-05-11</strong></summary>
**Released [v0.4.2](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.2)**
ChatGPT-Clone received some important upgrades and improvements. A new contributor, [@qcgm1978](https://github.com/qcgm1978), makes their first contribution by adding a null check for adaptiveCards variable. Additionally, support for titling conversations with the Azure endpoint is added by [@danny-avila](https://github.com/danny-avila) in PR [#234](https://github.com/danny-avila/chatgpt-clone/pull/234). In PR [#235](https://github.com/danny-avila/chatgpt-clone/pull/235), [@danny-avila](https://github.com/danny-avila) also makes some necessary fixes to titling, quotation marks, and endpoints being unavailable with only the Azure key provided. The logging system is now powered by Pino and sanitization, thanks to [@danorlando](https://github.com/danorlando) in PR [#227](https://github.com/danny-avila/chatgpt-clone/pull/227). To bulletproof the Docker container, the .dockerignore file is updated to include the client/.env file by [@danny-avila](https://github.com/danny-avila) in PR [#241](https://github.com/danny-avila/chatgpt-clone/pull/241). This issue was brought to our attention on discord.
There is active work on the new Plugins feature, converting the frontend to Typescript, and looking to integrate Palm2, google's new generative AI accessible via API, to the project as a new endpoint.
You can check the full changelog in between [v0.4.1](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.1) and [v0.4.2](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.2) [here](https://github.com/danny-avila/chatgpt-clone/compare/v0.4.1...v0.4.2)."
For discussion and suggestion you can join us: **[community discord server](https://discord.gg/NGaa9RPCft)**
</details>
<details>
<summary><strong>2023-05-09</strong></summary>
**Released [v0.4.1](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.1)**
* update user system section of readme by @danorlando in #207
* remove github-passport and update package.lock files by @danorlando in #208
* Update README.md by @fuegovic in #209
* fix: fix browser refresh redirecting to /chat/new by @danorlando in #210
* fix: fix issue with validation when google account has multiple spaces in username by @danorlando in #211
* chore: update docker image version to use latest by @danny-avila in #218
* update documentation structure by @fuegovic in #220
* Feat: Add Azure support by @danny-avila in #219
* Update Message.js by @DavidDev1334 in #191
⚠️ **IMPORTANT :** Since V0.4.0 You should register and login with a local account (email and password) for the first time sign-up. if you use login for the first time with a social login account (eg. Google, facebook, etc.), the conversations and presets that you created before the user system was implemented will NOT be migrated to that account.
⚠️ **Breaking - new Env Variables :** Since V0.4.0 You will need to add the new env variables from .env.example for the app to work, even if you're not using multiple users for your purposes.
For discussion and suggestion you can join us: **[community discord server](https://discord.gg/NGaa9RPCft)**
</details>
<details>
<summary><strong>2023-05-07</strong></summary>
**Released [v0.4.0](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.0)**, Introducing User/Auth System and OAuth2/Social Login! You can now register and login with an email account or use Google login. Your your previous conversations and presets will migrate to your new profile upon creation. Check out the details in the [User/Auth System](#userauth-system) section of the README.md.
@@ -10,11 +98,7 @@
For discussion and suggestion you can join us: **[community discord server](https://discord.gg/NGaa9RPCft)**
</details>
<details>
<summary><strong>Previous Updates</strong></summary>
<details>
<summary><strong>2023-04-05</strong></summary>
@@ -45,7 +129,7 @@ The above features are next and then I will have to focus on building the **test
On that note, I had to switch the default branch due to some breaking changes that haven't been straight forward to debug, mainly related to node-chat-gpt the main dependency of the project. Thankfully, my working branch, now switched to default as main, is working as expected.
</details>
</details>
##

View File

@@ -59,8 +59,8 @@ representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
https://t.me/proffapt.
reported to the community leaders responsible for enforcement here on GitHub or
on the official [Discord Server](https://discord.gg/uDyZ5Tzhct).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
@@ -129,4 +129,4 @@ https://www.contributor-covenant.org/translations.
##
## [Go Back to ReadMe](../../README.md)
## [Go Back to ReadMe](README.md)

View File

@@ -5,11 +5,19 @@ such as bug reports, documentation improvements, feature requests, and code cont
## Contributing Guidelines
When contributing to this repository, please first discuss the change you wish to make via [issue](https://github.com/danny-avila/chatgpt-clone/issues) or
join our discord [Discord community](https://discord.gg/NGaa9RPCft).
If the feature you would like to contribute has not already received prior approval from the project maintainers (ie. the feature is currently on the roadmap or on the [trello board]()), please submit a proposal in the [proposals category](https://github.com/danny-avila/chatgpt-clone/discussions/categories/proposals) of the discussions board before beginning work on it.
- Proposals should include specific implementation details including areas of the application that will be effected by the change inlcuding designs if applicable, and any other relevant information that might be required for a speedy review.
- Proposals are not required for small changes, bug fixes, or documentation improvements.
- Small changes and bug fixes should be tied to an [issue](https://github.com/danny-avila/chatgpt-clone/issues) and included in the corresponding pull request for tracking purposes.
*Please note that a pull request involving a feature that has not been reviewed and approved by the project maintainers may be rejected.*
If you would like to discuss the changes you wish to make, join our [Discord community](https://discord.gg/uDyZ5Tzhct).
## Our Standards
Please read our [Coding Standards and Conventions](docs/contributions/coding_conventions.md) before beginning on a contribution.
Examples of behavior that contributes to creating a positive environment
include:
@@ -172,6 +180,6 @@ Apply the following naming conventions to branches, labels, and other Git-relate
##
## [Go Back to ReadMe](../../README.md)
## [Go Back to ReadMe](README.md)

View File

@@ -1,25 +0,0 @@
# Contributors List
Here is a list of all contributors to this project:
- danny-avila (Admin)
- wtlyu (Contributor)
- danorlando (Contributor)
- alfredo-f (Contributor)
- HyunggyuJang (Contributor)
- fuegovic (Contributor)
- toordog (Contributor)
- heathriel (External Contributor)
- hackreactor-bot (Contributor)
- git-bruh (Contributor)
zhangsean (Contributor)
- llk89 (Contributor)
- adamrb (Contributor)
If you have contributed to this project and would like to be added to the list of contributors, please submit a pull request updating this file with your name and GitHub username.
##
## [Go Back to ReadMe](README.md)

View File

@@ -1,37 +1,32 @@
FROM node:19-alpine AS react-client
# Base node image
FROM node:19-alpine AS base
WORKDIR /api
COPY /api/package*.json /api/
WORKDIR /client
# copy package.json into the container at /client
COPY /client/package*.json /client/
# install dependencies
WORKDIR /
COPY /package*.json /
RUN npm ci
# Copy the current directory contents into the container at /client
# React client build
FROM base AS react-client
WORKDIR /client
COPY /client/ /client/
# Set the memory limit for Node.js
ENV NODE_OPTIONS="--max-old-space-size=2048"
# Build artifacts
RUN npm run build
FROM node:19-alpine AS node-api
# Node API setup
FROM base AS node-api
WORKDIR /api
# copy package.json into the container at /api
COPY /api/package*.json /api/
# install dependencies
RUN npm ci
# Copy the current directory contents into the container at /api
COPY /api/ /api/
# Copy the client side code
COPY --from=react-client /client/dist /client/dist
# Make port 3080 available to the world outside this container
EXPOSE 3080
# Expose the server to 0.0.0.0
ENV HOST=0.0.0.0
# Run the app when the container launches
CMD ["npm", "start"]
# Optional: for client with nginx routing
FROM nginx:stable-alpine AS nginx-client
WORKDIR /usr/share/nginx/html
COPY --from=react-client /client/dist /usr/share/nginx/html
# Add your nginx.conf
COPY /client/nginx.conf /etc/nginx/conf.d/default.conf
COPY client/nginx.conf /etc/nginx/conf.d/default.conf
ENTRYPOINT ["nginx", "-g", "daemon off;"]

View File

@@ -40,84 +40,99 @@
##
<details open>
<summary><strong>2023-05-07</strong></summary>
**Released [v0.4.0](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.0)**, Introducing User/Auth System and OAuth2/Social Login! You can now register and login with an email account or use Google login. Your your previous conversations and presets will migrate to your new profile upon creation. Check out the details in the [User/Auth System](documents/features/user_auth_system.md) section of the README.md.
## **Google's PaLM 2 is now supported as of [v0.4.3](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.4.3)**
![image](https://github.com/danny-avila/chatgpt-clone/assets/110412045/ec5e8ff3-6c3a-4f25-9687-d8558435d094)
⚠️ **IMPORTANT :** You should register and login with a local account (email and password) for the first time sign-up. if you use login for the first time with a social login account (eg. Google, facebook, etc.), the conversations and presets that you created before the user system was implemented will NOT be migrated to that account.
<details>
<summary><strong>How to Setup PaLM 2 (via Google Cloud Vertex AI API)</strong></summary>
- Enable the Vertex AI API on Google Cloud:
- - https://console.cloud.google.com/vertex-ai
- Create a Service Account:
- - https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create?walkthrough_id=iam--create-service-account#step_index=1
- Make sure to click 'Create and Continue' to give at least the 'Vertex AI User' role.
- Create a JSON key, rename as 'auth.json' and save it in /api/data/.
⚠️ **Breaking - new Env Variables :** You will need to add the new env variables from .env.example for the app to work, even if you're not using multiple users for your purposes.
**Alternatively**
For discussion and suggestion you can join us: **[community discord server](https://discord.gg/NGaa9RPCft)**
- In your ./api/.env file, set PALM_KEY as "user_provided" to allow the user to provide a Service Account key JSON from the UI.
- They will follow the steps above except for renaming the file, simply importing the JSON when prompted.
- The key is sent to the server but never saved except in your local storage
**Note:**
- Vertex AI does not (yet) support response streaming for text generations, so response may seem to take long when generating a lot of text.
- Text streaming is simulated
</details>
##
---
## [Read all Latest Updates here](CHANGELOG.md)
<h1>Table of Contents</h1>
<details open>
<summary><strong>Getting Started</strong></summary>
* [Docker Install](/documents/install/docker_install.md)
* [Linux Install](documents/install/linux_install.md)
* [Mac Install](documents/install/mac_install.md)
* [Windows Install](documents/install/windows_install.md)
* [Docker Install](/docs/install/docker_install.md)
* [Linux Install](docs/install/linux_install.md)
* [Mac Install](docs/install/mac_install.md)
* [Windows Install](docs/install/windows_install.md)
</details>
<details>
<summary><strong>General Information</strong></summary>
* [Project Origin](documents/general_info/project_origin.md)
* [Roadmap](documents/general_info/roadmap.md)
* [Tech Stack](documents/general_info/tech_stack.md)
* [Code of Conduct](CODE_OF_CONDUCT.md)
* [Project Origin](docs/general_info/project_origin.md)
* [Multilingual Information](docs/general_info/multilingual_information.md)
* [Roadmap](docs/general_info/roadmap.md)
* [Tech Stack](docs/general_info/tech_stack.md)
* [Changelog](CHANGELOG.md)
* [Bing Jailbreak Info](documents/general_info/bing_jailbreak_info.md)
* [Bing Jailbreak Info](docs/general_info/bing_jailbreak_info.md)
</details>
<details>
<summary><strong>Features</strong></summary>
* [User Auth System](documents/features/user_auth_system.md)
* [Proxy](documents/features/proxy.md)
* [User Auth System](docs/features/user_auth_system.md)
* [Proxy](docs/features/proxy.md)
</details>
<details>
<summary><strong>Cloud Deployment</strong></summary>
* [Heroku](documents/deployment/heroku.md)
* [Heroku](docs/deployment/heroku.md)
</details>
<details>
<summary><strong>Contributions</strong></summary>
* [Code of Conduct](documents/contributions/code_of_conduct.md)
* [Contributor Guidelines](documents/contributions/contributor_guidelines.md)
* [Documentation Guidelines](documents/contributions/documentation_guidelines.md)
* [Testing](documents/contributions/testing.md)
* [Pull Request Template](documents/contributions/pull_request_template.md)
* [Contributor Guidelines](CONTRIBUTING.md)
* [Documentation Guidelines](docs/contributions/documentation_guidelines.md)
* [Code Standards and Conventions](docs/contributions/coding_conventions.md)
* [Testing](docs/contributions/testing.md)
* [Security](SECURITY.md)
* [Trello Board](https://trello.com/b/17z094kq/chatgpt-clone)
</details>
<details>
<summary><strong>Report Templates</strong></summary>
* [Bug Report Template](documents/report_templates/bug_report_template.md)
* [Custom Issue Template](documents/report_templates/custom_issue_template.md)
* [Feature Request Template](documents/report_templates/feature_request_template.md)
</details>
##
## Contributing
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=danny-avila/chatgpt-clone&type=Date)](https://star-history.com/#danny-avila/chatgpt-clone&Date)
## Contributors
Contributions and suggestions bug reports and fixes are welcome!
Please read the documentation before you do!
For new features, components, or extensions, please open an issue and discuss before sending a PR.
- Join the [Discord community](https://discord.gg/NGaa9RPCft)
## License
This project is licensed under the [MIT License](LICENSE.md).
##
- Join the [Discord community](https://discord.gg/uDyZ5Tzhct)
This project exists in its current state thanks to all the people who contribute
---
<a href="https://github.com/danny-avila/chatgpt-clone/graphs/contributors">
<img src="https://contrib.rocks/image?repo=danny-avila/chatgpt-clone" />
</a>

55
SECURITY.md Normal file
View File

@@ -0,0 +1,55 @@
# 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/5rbRxn4uME) 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 `#issues` 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.
**Reference**
- https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html
##
## [Go Back to ReadMe](README.md)

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*.
@@ -58,8 +59,9 @@ OPENAI_MODELS=gpt-3.5-turbo,gpt-3.5-turbo-0301,text-davinci-003,gpt-4
##########################
# Also used for Sydney and jailbreak
# BingAI Tokens: the "_U" cookies value from bing.com
# To get your Access token for Bing, login to https://www.bing.com
# Use dev tools or an extension while logged into the site to copy the content of the _U cookie.
#If this fails, follow these instructions https://github.com/danny-avila/chatgpt-clone/issues/370#issuecomment-1560382302 to provide the full cookie strings.
# Set to "user_provided" to allow the user to provide its token from the UI.
# Leave it blank to disable this endpoint.
BINGAI_TOKEN="user_provided"
@@ -82,13 +84,43 @@ CHATGPT_TOKEN="user_provided"
# Identify the available models, separated by commas. The first will be default.
# Leave it blank to use internal settings.
CHATGPT_MODELS=text-davinci-002-render-sha,text-davinci-002-render-paid,gpt-4
CHATGPT_MODELS=text-davinci-002-render-sha,gpt-4
# NOTE: you can add gpt-4-plugins, gpt-4-code-interpreter, and gpt-4-browsing to the list above and use the models for these features;
# however, the view/display portion of these features are not supported, but you can use the underlying models, which have higher token context
# Also: text-davinci-002-render-paid is deprecated as of May 2023
# Reverse proxy settings for ChatGPT
# https://github.com/waylaidwanderer/node-chatgpt-api#using-a-reverse-proxy
# By default, the server will use the node-chatgpt-api recommended proxy (a third party server).
# CHATGPT_REVERSE_PROXY=
##########################
# PaLM (Google) Endpoint:
##########################
# PaLM 2 Client (via Google Cloud Vertex AI API)
# Steps:
# Enable the Vertex AI API on Google Cloud:
# https://console.cloud.google.com/vertex-ai
# Create a Service Account:
# https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create?walkthrough_id=iam--create-service-account#step_index=1
# Make sure to click 'Create and Continue' to give at least the 'Vertex AI User' role.
# Create a JSON key, rename as 'auth.json' and save it in /api/data/.
# Alternatively
# Uncomment below PALM_KEY and set as "user_provided" to allow the user to provide a Service Account key JSON from the UI.
# They will follow the steps above except for renaming the file.
# Leave blank or omit to disable this endpoint
# PALM_KEY="user_provided"
# In case you need a reverse proxy for this endpoint:
# GOOGLE_REVERSE_PROXY=
##########################
# Proxy: To be Used by all endpoints
##########################
PROXY=
##########################
# Search:
##########################

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

@@ -16,16 +16,17 @@ const askBing = async ({
token,
onProgress
}) => {
const { BingAIClient } = await import('og-chatgpt-api');
const { BingAIClient } = await import('@waylaidwanderer/chatgpt-api');
const store = {
store: new KeyvFile({ filename: './data/cache.json' })
};
const bingAIClient = new BingAIClient({
// "_U" cookie from bing.com
userToken: process.env.BINGAI_TOKEN == 'user_provided' ? token : process.env.BINGAI_TOKEN ?? null,
// userToken:
// process.env.BINGAI_TOKEN == 'user_provided' ? token : process.env.BINGAI_TOKEN ?? null,
// If the above doesn't work, provide all your cookies as a string instead
// cookies: '',
cookies: process.env.BINGAI_TOKEN == 'user_provided' ? token : process.env.BINGAI_TOKEN ?? null,
debug: false,
cache: store,
host: process.env.BINGAI_HOST || null,

View File

@@ -8,19 +8,22 @@ const browserClient = async ({
model,
token,
onProgress,
onEventMessage,
abortController,
userId
}) => {
const { ChatGPTBrowserClient } = await import('og-chatgpt-api');
const { ChatGPTBrowserClient } = await import('@waylaidwanderer/chatgpt-api');
const store = {
store: new KeyvFile({ filename: './data/cache.json' })
};
const clientOptions = {
// Warning: This will expose your access token to a third party. Consider the risks before using this.
reverseProxyUrl: process.env.CHATGPT_REVERSE_PROXY || 'https://ai.fakeopen.com/api/conversation',
reverseProxyUrl:
process.env.CHATGPT_REVERSE_PROXY || 'https://ai.fakeopen.com/api/conversation',
// Access token from https://chat.openai.com/api/auth/session
accessToken: process.env.CHATGPT_TOKEN == 'user_provided' ? token : process.env.CHATGPT_TOKEN ?? null,
accessToken:
process.env.CHATGPT_TOKEN == 'user_provided' ? token : process.env.CHATGPT_TOKEN ?? null,
model: model,
debug: false,
proxy: process.env.PROXY || null,
@@ -28,7 +31,7 @@ const browserClient = async ({
};
const client = new ChatGPTBrowserClient(clientOptions, store);
let options = { onProgress, abortController };
let options = { onProgress, onEventMessage, abortController };
if (!!parentMessageId && !!conversationId) {
options = { ...options, parentMessageId, conversationId };

View File

@@ -1,11 +1,16 @@
require('dotenv').config();
const { KeyvFile } = require('keyv-file');
const { genAzureEndpoint } = require('../../utils/genAzureEndpoints');
const tiktoken = require('@dqbd/tiktoken');
const tiktokenModels = require('../../utils/tiktokenModels');
const encoding_for_model = tiktoken.encoding_for_model;
const askClient = async ({
text,
parentMessageId,
conversationId,
model,
oaiApiKey,
chatGptLabel,
promptPrefix,
temperature,
@@ -22,12 +27,17 @@ const askClient = async ({
};
const azure = process.env.AZURE_OPENAI_API_KEY ? true : false;
let promptText = 'You are ChatGPT, a large language model trained by OpenAI.';
if (promptPrefix) {
promptText = promptPrefix;
}
const maxContextTokens = model === 'gpt-4' ? 8191 : model === 'gpt-4-32k' ? 32767 : 4095; // 1 less than maximum
const clientOptions = {
reverseProxyUrl: process.env.OPENAI_REVERSE_PROXY || null,
azure,
maxContextTokens,
modelOptions: {
model: model,
model,
temperature,
top_p,
presence_penalty,
@@ -35,26 +45,49 @@ const askClient = async ({
},
chatGptLabel,
promptPrefix,
proxy: process.env.PROXY || null,
debug: false
proxy: process.env.PROXY || null
// debug: true
};
let apiKey = process.env.OPENAI_KEY;
let apiKey = oaiApiKey ? oaiApiKey : process.env.OPENAI_KEY || null;
if (azure) {
apiKey = process.env.AZURE_OPENAI_API_KEY;
clientOptions.reverseProxyUrl = `https://${process.env.AZURE_OPENAI_API_INSTANCE_NAME}.openai.azure.com/openai/deployments/${process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME}/chat/completions?api-version=${process.env.AZURE_OPENAI_API_VERSION}`;
apiKey = oaiApiKey ? oaiApiKey : process.env.AZURE_OPENAI_API_KEY || null;
clientOptions.reverseProxyUrl = genAzureEndpoint({
azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME,
azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME,
azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION
});
}
const client = new ChatGPTClient(apiKey, clientOptions, store);
const options = {
onProgress,
abortController,
...(parentMessageId && conversationId ? { parentMessageId, conversationId } : {})
};
let usage = {};
let enc = null;
try {
enc = encoding_for_model(tiktokenModels.has(model) ? model : 'gpt-3.5-turbo');
usage.prompt_tokens = (enc.encode(promptText)).length + (enc.encode(text)).length;
} catch (e) {
console.log('Error encoding prompt text', e);
}
const res = await client.sendMessage(text, { ...options, userId });
try {
usage.completion_tokens = (enc.encode(res.response)).length;
enc.free();
usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
res.usage = usage;
} catch (e) {
console.log('Error encoding response text', e);
}
return res;
};

View File

@@ -0,0 +1,89 @@
require('dotenv').config();
const run = async () => {
const { ChatGPTClient } = await import('@waylaidwanderer/chatgpt-api');
const text = `
The standard Lorem Ipsum passage, used since the 1500s
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"
1914 translation by H. Rackham
"But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
"At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat."
1914 translation by H. Rackham
"On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple and easy to distinguish. In a free hour, when our power of choice is untrammelled and when nothing prevents our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he endures pains to avoid worse pains."
`;
const model = 'gpt-3.5-turbo';
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,
maxContextTokens,
modelOptions: {
model,
},
proxy: process.env.PROXY || null,
debug: true
};
let apiKey = process.env.OPENAI_KEY;
const maxMemory = 0.05 * 1024 * 1024 * 1024;
// Calculate initial percentage of memory used
const initialMemoryUsage = process.memoryUsage().heapUsed;
function printProgressBar(percentageUsed) {
const filledBlocks = Math.round(percentageUsed / 2); // Each block represents 2%
const emptyBlocks = 50 - filledBlocks; // Total blocks is 50 (each represents 2%), so the rest are empty
const progressBar = '[' + '█'.repeat(filledBlocks) + ' '.repeat(emptyBlocks) + '] ' + percentageUsed.toFixed(2) + '%';
console.log(progressBar);
}
const iterations = 16000;
console.time('loopTime');
// Trying to catch the error doesn't help; all future calls will immediately crash
for (let i = 0; i < iterations; i++) {
try {
console.log(`Iteration ${i}`);
const client = new ChatGPTClient(apiKey, clientOptions);
client.getTokenCount(text);
// const encoder = client.constructor.getTokenizer('cl100k_base');
// console.log(`Iteration ${i}: call encode()...`);
// encoder.encode(text, 'all');
// encoder.free();
const memoryUsageDuringLoop = process.memoryUsage().heapUsed;
const percentageUsed = memoryUsageDuringLoop / maxMemory * 100;
printProgressBar(percentageUsed);
if (i === (iterations - 1)) {
console.log(' done');
// encoder.free();
}
} catch (e) {
console.log(`caught error! in Iteration ${i}`);
console.log(e);
}
}
console.timeEnd('loopTime');
// Calculate final percentage of memory used
const finalMemoryUsage = process.memoryUsage().heapUsed;
// const finalPercentageUsed = finalMemoryUsage / maxMemory * 100;
console.log(`Initial memory usage: ${initialMemoryUsage / 1024 / 1024} megabytes`);
console.log(`Final memory usage: ${finalMemoryUsage / 1024 / 1024} megabytes`);
setTimeout(() => {
const memoryUsageAfterTimeout = process.memoryUsage().heapUsed;
console.log(`Post timeout: ${memoryUsageAfterTimeout / 1024 / 1024} megabytes`);
} , 10000);
}
run();

View File

@@ -0,0 +1,397 @@
const crypto = require('crypto');
const TextStream = require('../stream');
const { google } = require('googleapis');
const { Agent, ProxyAgent } = require('undici');
const { getMessages, saveMessage, saveConvo } = require('../../models');
const {
encoding_for_model: encodingForModel,
get_encoding: getEncoding
} = require('@dqbd/tiktoken');
const tokenizersCache = {};
class GoogleAgent {
constructor(credentials, options = {}) {
this.client_email = credentials.client_email;
this.project_id = credentials.project_id;
this.private_key = credentials.private_key;
this.setOptions(options);
this.currentDateString = new Date().toLocaleDateString('en-us', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
constructUrl() {
return `https://us-central1-aiplatform.googleapis.com/v1/projects/${this.project_id}/locations/us-central1/publishers/google/models/${this.modelOptions.model}:predict`;
}
setOptions(options) {
if (this.options && !this.options.replaceOptions) {
// nested options aren't spread properly, so we need to do this manually
this.options.modelOptions = {
...this.options.modelOptions,
...options.modelOptions
};
delete options.modelOptions;
// now we can merge options
this.options = {
...this.options,
...options
};
} else {
this.options = options;
}
this.options.examples = this.options.examples.filter(
(obj) => obj.input.content !== '' && obj.output.content !== ''
);
const modelOptions = this.options.modelOptions || {};
this.modelOptions = {
...modelOptions,
// set some good defaults (check for undefined in some cases because they may be 0)
model: modelOptions.model || 'chat-bison',
temperature: typeof modelOptions.temperature === 'undefined' ? 0.2 : modelOptions.temperature, // 0 - 1, 0.2 is recommended
topP: typeof modelOptions.topP === 'undefined' ? 0.95 : modelOptions.topP, // 0 - 1, default: 0.95
topK: typeof modelOptions.topK === 'undefined' ? 40 : modelOptions.topK // 1-40, default: 40
// stop: modelOptions.stop // no stop method for now
};
this.isChatModel = this.modelOptions.model.startsWith('chat-');
const { isChatModel } = this;
this.isTextModel = this.modelOptions.model.startsWith('text-');
const { isTextModel } = this;
this.maxContextTokens = this.options.maxContextTokens || (isTextModel ? 8000 : 4096);
// The max prompt tokens is determined by the max context tokens minus the max response tokens.
// Earlier messages will be dropped until the prompt is within the limit.
this.maxResponseTokens = this.modelOptions.maxOutputTokens || 1024;
this.maxPromptTokens =
this.options.maxPromptTokens || this.maxContextTokens - this.maxResponseTokens;
if (this.maxPromptTokens + this.maxResponseTokens > this.maxContextTokens) {
throw new Error(
`maxPromptTokens + maxOutputTokens (${this.maxPromptTokens} + ${this.maxResponseTokens} = ${
this.maxPromptTokens + this.maxResponseTokens
}) must be less than or equal to maxContextTokens (${this.maxContextTokens})`
);
}
this.userLabel = this.options.userLabel || 'User';
this.modelLabel = this.options.modelLabel || 'Assistant';
if (isChatModel) {
// Use these faux tokens to help the AI understand the context since we are building the chat log ourselves.
// Trying to use "<|im_start|>" causes the AI to still generate "<" or "<|" at the end sometimes for some reason,
// without tripping the stop sequences, so I'm using "||>" instead.
this.startToken = '||>';
this.endToken = '';
this.gptEncoder = this.constructor.getTokenizer('cl100k_base');
} else if (isTextModel) {
this.startToken = '<|im_start|>';
this.endToken = '<|im_end|>';
this.gptEncoder = this.constructor.getTokenizer('text-davinci-003', true, {
'<|im_start|>': 100264,
'<|im_end|>': 100265
});
} else {
// Previously I was trying to use "<|endoftext|>" but there seems to be some bug with OpenAI's token counting
// system that causes only the first "<|endoftext|>" to be counted as 1 token, and the rest are not treated
// as a single token. So we're using this instead.
this.startToken = '||>';
this.endToken = '';
try {
this.gptEncoder = this.constructor.getTokenizer(this.modelOptions.model, true);
} catch {
this.gptEncoder = this.constructor.getTokenizer('text-davinci-003', true);
}
}
if (!this.modelOptions.stop) {
const stopTokens = [this.startToken];
if (this.endToken && this.endToken !== this.startToken) {
stopTokens.push(this.endToken);
}
stopTokens.push(`\n${this.userLabel}:`);
stopTokens.push('<|diff_marker|>');
// I chose not to do one for `modelLabel` because I've never seen it happen
this.modelOptions.stop = stopTokens;
}
if (this.options.reverseProxyUrl) {
this.completionsUrl = this.options.reverseProxyUrl;
} else {
this.completionsUrl = this.constructUrl();
}
return this;
}
static getTokenizer(encoding, isModelName = false, extendSpecialTokens = {}) {
if (tokenizersCache[encoding]) {
return tokenizersCache[encoding];
}
let tokenizer;
if (isModelName) {
tokenizer = encodingForModel(encoding, extendSpecialTokens);
} else {
tokenizer = getEncoding(encoding, extendSpecialTokens);
}
tokenizersCache[encoding] = tokenizer;
return tokenizer;
}
async getClient() {
const scopes = ['https://www.googleapis.com/auth/cloud-platform'];
const jwtClient = new google.auth.JWT(this.client_email, null, this.private_key, scopes);
jwtClient.authorize((err) => {
if (err) {
console.log(err);
throw err;
}
});
return jwtClient;
}
buildPayload(input, { messages = [] }) {
let payload = {
instances: [
{
messages: [...messages, { author: this.userLabel, content: input }]
}
],
parameters: this.options.modelOptions
};
if (this.options.promptPrefix) {
payload.instances[0].context = this.options.promptPrefix;
}
if (this.options.examples.length > 0) {
payload.instances[0].examples = this.options.examples;
}
if (this.isTextModel) {
payload.instances = [
{
prompt: input
}
];
}
if (this.options.debug) {
console.debug('buildPayload');
console.dir(payload, { depth: null });
}
return payload;
}
async getCompletion(input, messages = [], abortController = null) {
if (!abortController) {
abortController = new AbortController();
}
const { debug } = this.options;
const url = this.completionsUrl;
if (debug) {
console.debug();
console.debug(url);
console.debug(this.modelOptions);
console.debug();
}
const opts = {
method: 'POST',
agent: new Agent({
bodyTimeout: 0,
headersTimeout: 0
}),
signal: abortController.signal
};
if (this.options.proxy) {
opts.agent = new ProxyAgent(this.options.proxy);
}
const client = await this.getClient();
const payload = this.buildPayload(input, { messages });
const res = await client.request({ url, method: 'POST', data: payload });
console.dir(res.data, { depth: null });
return res.data;
}
async loadHistory(conversationId, parentMessageId = null) {
if (this.options.debug) {
console.debug('Loading history for conversation', conversationId, parentMessageId);
}
if (!parentMessageId) {
return [];
}
const messages = (await getMessages({ conversationId })) || [];
if (messages.length === 0) {
this.currentMessages = [];
return [];
}
const orderedMessages = this.constructor.getMessagesForConversation(messages, parentMessageId);
return orderedMessages.map((message) => {
return {
author: message.isCreatedByUser ? this.userLabel : this.modelLabel,
content: message.content
};
});
}
async saveMessageToDatabase(message, user = null) {
await saveMessage({ ...message, unfinished: false });
await saveConvo(user, {
conversationId: message.conversationId,
endpoint: 'google',
...this.modelOptions
});
}
async sendMessage(message, opts = {}) {
if (opts && typeof opts === 'object') {
this.setOptions(opts);
}
console.log('sendMessage', message, opts);
const user = opts.user || null;
const conversationId = opts.conversationId || crypto.randomUUID();
const parentMessageId = opts.parentMessageId || '00000000-0000-0000-0000-000000000000';
const userMessageId = opts.overrideParentMessageId || crypto.randomUUID();
const responseMessageId = crypto.randomUUID();
const messages = await this.loadHistory(conversationId, this.options?.parentMessageId);
const userMessage = {
messageId: userMessageId,
parentMessageId,
conversationId,
sender: 'User',
text: message,
isCreatedByUser: true
};
if (typeof opts?.getIds === 'function') {
opts.getIds({
userMessage,
conversationId,
responseMessageId
});
}
console.log('userMessage', userMessage);
await this.saveMessageToDatabase(userMessage, user);
let reply = '';
let blocked = false;
try {
const result = await this.getCompletion(message, messages, opts.abortController);
blocked = result?.predictions?.[0]?.safetyAttributes?.blocked;
reply =
result?.predictions?.[0]?.candidates?.[0]?.content ||
result?.predictions?.[0]?.content ||
'';
if (blocked === true) {
reply = `Google blocked a proper response to your message:\n${JSON.stringify(
result.predictions[0].safetyAttributes
)}${reply.length > 0 ? `\nAI Response:\n${reply}` : ''}`;
}
if (this.options.debug) {
console.debug('result');
console.debug(result);
}
} catch (err) {
console.error(err);
}
if (this.options.debug) {
console.debug('options');
console.debug(this.options);
}
if (!blocked) {
const textStream = new TextStream(reply, { delay: 0.5 });
await textStream.processTextStream(opts.onProgress);
}
const responseMessage = {
messageId: responseMessageId,
conversationId,
parentMessageId: userMessage.messageId,
sender: 'PaLM2',
text: reply,
error: blocked,
isCreatedByUser: false
};
await this.saveMessageToDatabase(responseMessage, user);
return responseMessage;
}
getTokenCount(text) {
return this.gptEncoder.encode(text, 'all').length;
}
/**
* Algorithm adapted from "6. Counting tokens for chat API calls" of
* https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
*
* An additional 2 tokens need to be added for metadata after all messages have been counted.
*
* @param {*} message
*/
getTokenCountForMessage(message) {
// Map each property of the message to the number of tokens it contains
const propertyTokenCounts = Object.entries(message).map(([key, value]) => {
// Count the number of tokens in the property value
const numTokens = this.getTokenCount(value);
// Subtract 1 token if the property key is 'name'
const adjustment = key === 'name' ? 1 : 0;
return numTokens - adjustment;
});
// Sum the number of tokens in all properties and add 4 for metadata
return propertyTokenCounts.reduce((a, b) => a + b, 4);
}
/**
* Iterate through messages, building an array based on the parentMessageId.
* Each message has an id and a parentMessageId. The parentMessageId is the id of the message that this message is a reply to.
* @param messages
* @param parentMessageId
* @returns {*[]} An array containing the messages in the order they should be displayed, starting with the root message.
*/
static getMessagesForConversation(messages, parentMessageId) {
const orderedMessages = [];
let currentMessageId = parentMessageId;
while (currentMessageId) {
// eslint-disable-next-line no-loop-func
const message = messages.find((m) => m.messageId === currentMessageId);
if (!message) {
break;
}
orderedMessages.unshift(message);
currentMessageId = message.parentMessageId;
}
if (orderedMessages.length === 0) {
return [];
}
return orderedMessages.map((msg) => ({
isCreatedByUser: msg.isCreatedByUser,
content: msg.text
}));
}
}
module.exports = GoogleAgent;

59
api/app/stream.js Normal file
View File

@@ -0,0 +1,59 @@
const { Readable } = require('stream');
class TextStream extends Readable {
constructor(text, options = {}) {
super(options);
this.text = text;
this.currentIndex = 0;
this.delay = options.delay || 20; // Time in milliseconds
}
_read() {
const minChunkSize = 2;
const maxChunkSize = 4;
const { delay } = this;
if (this.currentIndex < this.text.length) {
setTimeout(() => {
const remainingChars = this.text.length - this.currentIndex;
const chunkSize = Math.min(this.randomInt(minChunkSize, maxChunkSize + 1), remainingChars);
const chunk = this.text.slice(this.currentIndex, this.currentIndex + chunkSize);
this.push(chunk);
this.currentIndex += chunkSize;
}, delay);
} else {
this.push(null); // signal end of data
}
}
randomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
async processTextStream(onProgressCallback) {
const streamPromise = new Promise((resolve, reject) => {
this.on('data', (chunk) => {
onProgressCallback(chunk.toString());
});
this.on('end', () => {
console.log('Stream ended');
resolve();
});
this.on('error', (err) => {
reject(err);
});
});
try {
await streamPromise;
} catch (err) {
console.error('Error processing text stream:', err);
// Handle the error appropriately, e.g., return an error message or throw an error
}
}
}
module.exports = TextStream;

View File

@@ -1,7 +1,8 @@
const { Configuration, OpenAIApi } = require('openai');
const _ = require('lodash');
const { genAzureEndpoint } = require('../utils/genAzureEndpoints');
const proxyEnvToAxiosProxy = proxyString => {
const proxyEnvToAxiosProxy = (proxyString) => {
if (!proxyString) return null;
const regex = /^([^:]+):\/\/(?:([^:@]*):?([^:@]*)@)?([^:]+)(?::(\d+))?/;
@@ -16,7 +17,7 @@ const proxyEnvToAxiosProxy = proxyString => {
return proxyConfig;
};
const titleConvo = async ({ endpoint, text, response }) => {
const titleConvo = async ({ endpoint, text, response, oaiApiKey }) => {
let title = 'New Chat';
const ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default;
@@ -33,7 +34,9 @@ const titleConvo = async ({ endpoint, text, response }) => {
||>Title:`
};
const azure = process.env.AZURE_OPENAI_API_KEY ? true : false;
const options = {
azure,
reverseProxyUrl: process.env.OPENAI_REVERSE_PROXY || null,
proxy: process.env.PROXY || null
};
@@ -47,9 +50,20 @@ const titleConvo = async ({ endpoint, text, response }) => {
frequency_penalty: 0
};
const titleGenClient = new ChatGPTClient(process.env.OPENAI_KEY, titleGenClientOptions);
let apiKey = oaiApiKey || process.env.OPENAI_KEY;
if (azure) {
apiKey = process.env.AZURE_OPENAI_API_KEY;
titleGenClientOptions.reverseProxyUrl = genAzureEndpoint({
azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME,
azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME,
azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION
});
}
const titleGenClient = new ChatGPTClient(apiKey, titleGenClientOptions);
const result = await titleGenClient.getCompletion([instructionsPayload], null);
title = result.choices[0].message.content.replace(/\s+/g, ' ').trim();
title = result.choices[0].message.content.replace(/\s+/g, ' ').replaceAll('"', '').trim();
} catch (e) {
console.error(e);
console.log('There was an issue generating title, see error above');

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

@@ -2,7 +2,8 @@
const regex = / \[.*?]\(.*?\)/g;
const getCitations = (res) => {
const textBlocks = res.details.adaptiveCards[0].body;
const adaptiveCards = res.details.adaptiveCards;
const textBlocks = adaptiveCards && adaptiveCards[0].body;
if (!textBlocks) return '';
let links = textBlocks[textBlocks.length - 1]?.text.match(regex);
if (links?.length === 0 || !links) return '';
@@ -10,4 +11,4 @@ const getCitations = (res) => {
return links.join('\n');
};
module.exports = getCitations;
module.exports = getCitations;

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

@@ -30,7 +30,7 @@ module.exports = {
return { message: 'Error saving conversation' };
}
},
getConvosByPage: async (user, pageNumber = 1, pageSize = 12) => {
getConvosByPage: async (user, pageNumber = 1, pageSize = 14) => {
try {
const totalConvos = (await Conversation.countDocuments({ user })) || 1;
const totalPages = Math.ceil(totalConvos / pageSize);
@@ -45,7 +45,7 @@ module.exports = {
return { message: 'Error getting conversations' };
}
},
getConvosQueried: async (user, convoIds, pageNumber = 1, pageSize = 12) => {
getConvosQueried: async (user, convoIds, pageNumber = 1, pageSize = 14) => {
try {
if (!convoIds || convoIds.length === 0) {
return { conversations: [], pages: 1, pageNumber, pageSize };

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

@@ -17,6 +17,12 @@ module.exports = {
default: null,
required: false
},
// for google only
modelLabel: {
type: String,
default: null,
required: false
},
promptPrefix: {
type: String,
default: null,
@@ -32,6 +38,22 @@ module.exports = {
default: 1,
required: false
},
// for google only
topP: {
type: Number,
default: 0.95,
required: false
},
topK: {
type: Number,
default: 40,
required: false
},
maxOutputTokens: {
type: Number,
default: 1024,
required: false
},
presence_penalty: {
type: Number,
default: 0,

View File

@@ -20,8 +20,14 @@ const convoSchema = mongoose.Schema(
default: null
},
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }],
// google only
examples: [{ type: mongoose.Schema.Types.Mixed }],
...conversationPreset,
// for bingAI only
bingConversationId: {
type: String,
default: null
},
jailbreakConversationId: {
type: String,
default: null

View File

@@ -17,6 +17,8 @@ const presetSchema = mongoose.Schema(
type: String,
default: null
},
// google only
examples: [{ type: mongoose.Schema.Types.Mixed }],
...conversationPreset
},
{ timestamps: true }

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

10967
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "chatgpt-clone",
"version": "0.4.1",
"name": "chat-backend",
"version": "0.4.7",
"description": "",
"main": "server/index.js",
"scripts": {
@@ -21,34 +21,33 @@
"dependencies": {
"@dqbd/tiktoken": "^1.0.2",
"@keyv/mongo": "^2.1.8",
"@waylaidwanderer/chatgpt-api": "github:danny-avila/node-chatgpt-api",
"@waylaidwanderer/chatgpt-api": "^1.37.0",
"axios": "^1.3.4",
"bcrypt": "^5.1.0",
"bcryptjs": "^2.4.3",
"cookie": "^0.5.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"dotenv": "^16.0.3",
"eslint": "^8.36.0",
"eslint": "^8.41.0",
"express": "^4.18.2",
"googleapis": "^118.0.0",
"handlebars": "^4.7.7",
"html": "^1.0.0",
"joi": "^14.3.1",
"joi": "^17.9.2",
"jsonwebtoken": "^9.0.0",
"keyv": "^4.5.2",
"keyv-file": "^0.2.0",
"lodash": "^4.17.21",
"meilisearch": "^0.31.1",
"mongoose": "^6.9.0",
"meilisearch": "^0.33.0",
"mongoose": "^7.1.1",
"nodemailer": "^6.9.1",
"og-chatgpt-api": "npm:@waylaidwanderer/chatgpt-api@^1.35.0",
"openai": "^3.1.0",
"passport": "^0.6.0",
"passport-facebook": "^3.0.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pino": "^8.12.1",
"sanitize": "^2.1.2"
},
"devDependencies": {

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

@@ -40,7 +40,7 @@ router.post('/', requireJwtAuth, async (req, res) => {
jailbreakConversationId: req.body?.jailbreakConversationId ?? null,
systemMessage: req.body?.systemMessage ?? null,
context: req.body?.context ?? null,
toneStyle: req.body?.toneStyle ?? 'fast',
toneStyle: req.body?.toneStyle ?? 'creative',
token: req.body?.token ?? null
};
else
@@ -51,7 +51,7 @@ router.post('/', requireJwtAuth, async (req, res) => {
conversationSignature: req.body?.conversationSignature ?? null,
clientId: req.body?.clientId ?? null,
invocationId: req.body?.invocationId ?? null,
toneStyle: req.body?.toneStyle ?? 'fast',
toneStyle: req.body?.toneStyle ?? 'creative',
token: req.body?.token ?? null
};
@@ -110,7 +110,7 @@ const ask = async ({
try {
let lastSavedTimestamp = 0;
const { onProgress: progressCallback, getPartialText } = createOnProgress({
const { onProgress: progressCallback } = createOnProgress({
onProgress: ({ text }) => {
const currentTimestamp = Date.now();
if (currentTimestamp - lastSavedTimestamp > 500) {
@@ -129,10 +129,15 @@ const ask = async ({
}
});
const abortController = new AbortController();
let bingConversationId = null;
if (!isNewConversation) {
const convo = await getConvo(req.user.id, conversationId);
bingConversationId = convo.bingConversationId;
}
let response = await askBing({
text,
parentMessageId: userParentMessageId,
conversationId,
conversationId: bingConversationId ?? conversationId,
...endpointOption,
onProgress: progressCallback.call(null, {
res,
@@ -147,21 +152,25 @@ const ask = async ({
const newConversationId = endpointOption?.jailbreak
? response.jailbreakConversationId
: response.conversationId || conversationId;
const newUserMassageId = response.parentMessageId || response.details.requestId || userMessageId;
const newUserMessageId =
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,
conversationId,
bingConversationId: newConversationId,
messageId: responseMessageId,
newMessageId: newResponseMessageId,
parentMessageId: overrideParentMessageId || newUserMassageId,
parentMessageId: overrideParentMessageId || newUserMessageId,
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
@@ -170,31 +179,7 @@ const ask = async ({
await saveMessage(responseMessage);
responseMessage.messageId = newResponseMessageId;
// STEP2 update the convosation.
// First update conversationId if needed
// Note!
// Bing API will not use our conversationId at the first time,
// so change the placeholder conversationId to the real one.
// Attition: the api will also create new conversationId while using invalid userMessage.parentMessageId,
// but in this situation, don't change the conversationId, but create new convo.
let conversationUpdate = { conversationId: newConversationId, endpoint: 'bingAI' };
if (conversationId != newConversationId)
if (isNewConversation) {
// change the conversationId to new one
conversationUpdate = {
...conversationUpdate,
conversationId: conversationId,
newConversationId: newConversationId
};
} else {
// create new conversation
conversationUpdate = {
...conversationUpdate,
...endpointOption
};
}
let conversationUpdate = { conversationId, bingConversationId: newConversationId, endpoint: 'bingAI' };
if (endpointOption?.jailbreak) {
conversationUpdate.jailbreak = true;
@@ -207,16 +192,16 @@ const ask = async ({
}
await saveConvo(req.user.id, conversationUpdate);
conversationId = newConversationId;
// STEP3 update the user message
userMessage.conversationId = newConversationId;
userMessage.messageId = newUserMassageId;
userMessage.messageId = newUserMessageId;
// If response has parentMessageId, the fake userMessage.messageId should be updated to the real one.
if (!overrideParentMessageId)
await saveMessage({ ...userMessage, messageId: userMessageId, newMessageId: newUserMassageId });
userMessageId = newUserMassageId;
await saveMessage({
...userMessage,
messageId: userMessageId,
newMessageId: newUserMessageId
});
userMessageId = newUserMessageId;
sendMessage(res, {
title: await getConvoTitle(req.user.id, conversationId),
@@ -228,7 +213,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

@@ -76,7 +76,6 @@ const ask = async ({
userMessage,
endpointOption,
conversationId,
preSendRequest = true,
overrideParentMessageId = null,
req,
res
@@ -92,10 +91,8 @@ const ask = async ({
'X-Accel-Buffering': 'no'
});
if (preSendRequest) sendMessage(res, { message: userMessage, created: true });
let responseMessageId = crypto.randomUUID();
let getPartialMessage = null;
try {
let lastSavedTimestamp = 0;
const { onProgress: progressCallback, getPartialText } = createOnProgress({
@@ -116,15 +113,30 @@ const ask = async ({
}
}
});
getPartialMessage = getPartialText;
const abortController = new AbortController();
let response = await browserClient({
text,
parentMessageId: userParentMessageId,
conversationId,
...endpointOption,
onProgress: progressCallback.call(null, { res, text }),
abortController,
userId
userId,
onProgress: progressCallback.call(null, { res, text }),
onEventMessage: (eventMessage) => {
let data = null;
try {
data = JSON.parse(eventMessage.data);
} catch (e) {
return;
}
sendMessage(res, {
message: { ...userMessage, conversationId: data.conversation_id },
created: true
});
}
});
console.log('CLIENT RESPONSE', response);
@@ -180,7 +192,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, {
@@ -208,8 +224,8 @@ const ask = async ({
parentMessageId: overrideParentMessageId || userMessageId,
unfinished: false,
cancelled: false,
error: true,
text: error.message
// error: true,
text: `${getPartialMessage() ?? ''}\n\nError message: "${error.message}"`
};
await saveMessage(errorMessage);
handleError(res, errorMessage);

View File

@@ -0,0 +1,178 @@
const express = require('express');
const router = express.Router();
const crypto = require('crypto');
const { titleConvo } = require('../../../app/');
const GoogleClient = require('../../../app/google/GoogleClient');
const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('../../../models');
const { handleError, sendMessage, createOnProgress } = require('./handlers');
const requireJwtAuth = require('../../../middleware/requireJwtAuth');
router.post('/', requireJwtAuth, async (req, res) => {
const { endpoint, text, parentMessageId, conversationId: oldConversationId } = req.body;
if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' });
if (endpoint !== 'google') return handleError(res, { text: 'Illegal request' });
// build endpoint option
const endpointOption = {
examples: req.body?.examples ?? [{ input: { content: '' }, output: { content: '' } }],
promptPrefix: req.body?.promptPrefix ?? null,
token: req.body?.token ?? null,
modelOptions: {
model: req.body?.model ?? 'chat-bison',
modelLabel: req.body?.modelLabel ?? null,
temperature: req.body?.temperature ?? 0.2,
maxOutputTokens: req.body?.maxOutputTokens ?? 1024,
topP: req.body?.topP ?? 0.95,
topK: req.body?.topK ?? 40
}
};
const availableModels = ['chat-bison', 'text-bison'];
if (availableModels.find((model) => model === endpointOption.modelOptions.model) === undefined) {
return handleError(res, { text: `Illegal request: model` });
}
const conversationId = oldConversationId || crypto.randomUUID();
// eslint-disable-next-line no-use-before-define
return await ask({
text,
endpointOption,
conversationId,
parentMessageId,
req,
res
});
});
const ask = async ({ text, endpointOption, parentMessageId = null, conversationId, req, res }) => {
res.writeHead(200, {
Connection: 'keep-alive',
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'Access-Control-Allow-Origin': '*',
'X-Accel-Buffering': 'no'
});
let userMessage;
let userMessageId;
let responseMessageId;
let lastSavedTimestamp = 0;
const { overrideParentMessageId = null } = req.body;
try {
const getIds = (data) => {
userMessage = data.userMessage;
userMessageId = userMessage.messageId;
responseMessageId = data.responseMessageId;
if (!conversationId) {
conversationId = data.conversationId;
}
sendMessage(res, { message: userMessage, created: true });
};
const { onProgress: progressCallback } = createOnProgress({
onProgress: ({ text: partialText }) => {
const currentTimestamp = Date.now();
if (currentTimestamp - lastSavedTimestamp > 500) {
lastSavedTimestamp = currentTimestamp;
saveMessage({
messageId: responseMessageId,
sender: 'PaLM2',
conversationId,
parentMessageId: overrideParentMessageId || userMessageId,
text: partialText,
unfinished: true,
cancelled: false,
error: false
});
}
}
});
const abortController = new AbortController();
let key;
if (endpointOption.token) {
key = JSON.parse(endpointOption.token);
delete endpointOption.token;
console.log('Using service account key provided by User for PaLM models');
}
try {
if (!key) {
key = require('../../../data/auth.json');
}
} catch (e) {
console.log("No 'auth.json' file (service account key) found in /api/data/ for PaLM models");
}
const clientOptions = {
// debug: true, // for testing
reverseProxyUrl: process.env.GOOGLE_REVERSE_PROXY || null,
proxy: process.env.PROXY || null,
...endpointOption
};
const client = new GoogleClient(key, clientOptions);
let response = await client.sendMessage(text, {
getIds,
user: req.user.id,
conversationId,
parentMessageId,
overrideParentMessageId,
onProgress: progressCallback.call(null, {
res,
text,
parentMessageId: overrideParentMessageId || userMessageId
}),
abortController
});
if (overrideParentMessageId) {
response.parentMessageId = overrideParentMessageId;
}
await saveConvo(req.user.id, {
...endpointOption,
...endpointOption.modelOptions,
conversationId,
endpoint: 'google'
});
await saveMessage(response);
sendMessage(res, {
title: await getConvoTitle(req.user.id, conversationId),
final: true,
conversation: await getConvo(req.user.id, conversationId),
requestMessage: userMessage,
responseMessage: response
});
res.end();
if (parentMessageId == '00000000-0000-0000-0000-000000000000') {
const title = await titleConvo({ text, response });
await saveConvo(req.user.id, {
conversationId,
title
});
}
} catch (error) {
console.error(error);
const errorMessage = {
messageId: responseMessageId,
sender: 'PaLM2',
conversationId,
parentMessageId,
unfinished: false,
cancelled: false,
error: true,
text: error.message
};
await saveMessage(errorMessage);
handleError(res, errorMessage);
}
};
module.exports = router;

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,
@@ -188,7 +190,7 @@ const ask = async ({
console.log('CLIENT RESPONSE', response);
const newConversationId = response.conversationId || conversationId;
const newUserMassageId = response.parentMessageId || userMessageId;
const newUserMessageId = response.parentMessageId || userMessageId;
const newResponseMessageId = response.messageId;
// STEP1 generate response message
@@ -198,7 +200,7 @@ const ask = async ({
conversationId: newConversationId,
messageId: responseMessageId,
newMessageId: newResponseMessageId,
parentMessageId: overrideParentMessageId || newUserMassageId,
parentMessageId: overrideParentMessageId || newUserMessageId,
text: await handleText(response),
sender: endpointOption?.chatGptLabel || 'ChatGPT',
unfinished: false,
@@ -232,12 +234,16 @@ const ask = async ({
// STEP3 update the user message
userMessage.conversationId = newConversationId;
userMessage.messageId = newUserMassageId;
userMessage.messageId = newUserMessageId;
// If response has parentMessageId, the fake userMessage.messageId should be updated to the real one.
if (!overrideParentMessageId)
await saveMessage({ ...userMessage, messageId: userMessageId, newMessageId: newUserMassageId });
userMessageId = newUserMassageId;
await saveMessage({
...userMessage,
messageId: userMessageId,
newMessageId: newUserMessageId
});
userMessageId = newUserMessageId;
sendMessage(res, {
title: await getConvoTitle(req.user.id, conversationId),
@@ -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

@@ -2,11 +2,13 @@ const express = require('express');
const router = express.Router();
// const askAzureOpenAI = require('./askAzureOpenAI';)
const askOpenAI = require('./askOpenAI');
const askGoogle = require('./askGoogle');
const askBingAI = require('./askBingAI');
const askChatGPTBrowser = require('./askChatGPTBrowser');
// router.use('/azureOpenAI', askAzureOpenAI);
router.use('/openAI', askOpenAI);
router.use('/google', askGoogle);
router.use('/bingAI', askBingAI);
router.use('/chatGPTBrowser', askChatGPTBrowser);

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

@@ -9,15 +9,41 @@ const getOpenAIModels = () => {
};
const getChatGPTBrowserModels = () => {
let models = ['text-davinci-002-render-sha', 'text-davinci-002-render-paid', 'gpt-4'];
let models = ['text-davinci-002-render-sha', 'gpt-4'];
if (process.env.CHATGPT_MODELS) models = String(process.env.CHATGPT_MODELS).split(',');
return models;
};
router.get('/', function (req, res) {
let i = 0;
router.get('/', async function (req, res) {
let key, palmUser;
try {
key = require('../../data/auth.json');
} catch (e) {
if (i === 0) {
console.log("No 'auth.json' file (service account key) found in /api/data/ for PaLM models");
i++;
}
}
if (process.env.PALM_KEY === 'user_provided') {
palmUser = true;
if (i <= 1) {
console.log('User will provide key for PaLM models');
i++;
}
}
const google =
key || palmUser
? { userProvide: palmUser, availableModels: ['chat-bison', 'text-bison'] }
: false;
const azureOpenAI = !!process.env.AZURE_OPENAI_KEY;
const openAI = process.env.OPENAI_KEY ? { availableModels: getOpenAIModels() } : false;
const apiKey = process.env.OPENAI_KEY || process.env.AZURE_OPENAI_API_KEY;
const openAI = apiKey
? { availableModels: getOpenAIModels(), userProvide: apiKey === 'user_provided' }
: false;
const bingAI = process.env.BINGAI_TOKEN
? { userProvide: process.env.BINGAI_TOKEN == 'user_provided' }
: false;
@@ -28,7 +54,7 @@ router.get('/', function (req, res) {
}
: false;
res.send(JSON.stringify({ azureOpenAI, openAI, bingAI, chatGPTBrowser }));
res.send(JSON.stringify({ azureOpenAI, openAI, google, bingAI, chatGPTBrowser }));
});
module.exports = { router, getOpenAIModels, getChatGPTBrowserModels };

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

@@ -2,9 +2,8 @@ const User = require('../../models/User');
const Token = require('../../models/schema/tokenSchema');
const sendEmail = require('../../utils/sendEmail');
const crypto = require('crypto');
const bcrypt = require('bcrypt');
const bcrypt = require('bcryptjs');
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,26 +84,19 @@ const registerUser = async (user) => {
username,
name,
avatar: null,
role: isFirstRegisteredUser ? 'ADMIN' : 'USER',
role: isFirstRegisteredUser ? 'ADMIN' : 'USER'
});
// todo: implement refresh token
// const refreshToken = newUser.generateRefreshToken();
// newUser.refreshToken.push({ refreshToken });
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(newUser.password, salt, (errh, hash) => {
if (err) {
console.log(err);
}
// set pasword to hash
newUser.password = hash;
newUser.save();
});
});
console.log('newUser', newUser)
const salt = bcrypt.genSaltSync(10);
const hash = bcrypt.hashSync(newUser.password, salt);
newUser.password = hash;
newUser.save();
if (isFirstRegisteredUser) {
migrateDataToFirstUser(newUser);
// console.log(migrate);
}
response = { status: 200, user: newUser };
return response;
@@ -131,7 +120,7 @@ const requestPasswordReset = async (email) => {
if (token) await token.deleteOne();
let resetToken = crypto.randomBytes(32).toString('hex');
const hash = await bcrypt.hash(resetToken, 10);
const hash = await bcrypt.hashSync(resetToken, 10);
await new Token({
userId: user._id,
@@ -160,13 +149,13 @@ const resetPassword = async (userId, token, password) => {
return new Error('Invalid or expired password reset token');
}
const isValid = await bcrypt.compare(token, passwordResetToken.token);
const isValid = bcrypt.compareSync(token, passwordResetToken.token);
if (!isValid) {
return new Error('Invalid or expired password reset token');
}
const hash = await bcrypt.hash(password, 10);
const hash = bcrypt.hashSync(password, 10);
await User.updateOne({ _id: userId }, { $set: { password: hash } }, { new: true });
@@ -186,12 +175,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
};
};

125
api/utils/LoggingSystem.js Normal file
View File

@@ -0,0 +1,125 @@
const pino = require('pino');
const logger = pino({
level: 'info',
redact: {
paths: [
// List of Paths to redact from the logs (https://getpino.io/#/docs/redaction)
'env.OPENAI_KEY',
'env.BINGAI_TOKEN',
'env.CHATGPT_TOKEN',
'env.MEILI_MASTER_KEY',
'env.GOOGLE_CLIENT_SECRET',
'env.JWT_SECRET_DEV',
'env.JWT_SECRET_PROD',
'newUser.password'
], // See example to filter object class instances
censor: '***' // Redaction character
}
});
// Sanitize outside the logger paths. This is useful for sanitizing variables directly with Regex and patterns.
const redactPatterns = [
// Array of regular expressions for redacting patterns
/api[-_]?key/i,
/password/i,
/token/i,
/secret/i,
/key/i,
/certificate/i,
/client[-_]?id/i,
/authorization[-_]?code/i,
/authorization[-_]?login[-_]?hint/i,
/authorization[-_]?acr[-_]?values/i,
/authorization[-_]?response[-_]?mode/i,
/authorization[-_]?nonce/i
];
/*
// Example of redacting sensitive data from object class instances
function redactSensitiveData(obj) {
if (obj instanceof User) {
return {
...obj.toObject(),
password: '***', // Redact the password field
};
}
return obj;
}
// Example of redacting sensitive data from object class instances
logger.info({ newUser: redactSensitiveData(newUser) }, 'newUser');
*/
const levels = {
TRACE: 10,
DEBUG: 20,
INFO: 30,
WARN: 40,
ERROR: 50,
FATAL: 60
};
let level = levels.INFO;
module.exports = {
levels,
setLevel: (l) => (level = l),
log: {
trace: (msg) => {
if (level <= levels.TRACE) return;
logger.trace(msg);
},
debug: (msg) => {
if (level <= levels.DEBUG) return;
logger.debug(msg);
},
info: (msg) => {
if (level <= levels.INFO) return;
logger.info(msg);
},
warn: (msg) => {
if (level <= levels.WARN) return;
logger.warn(msg);
},
error: (msg) => {
if (level <= levels.ERROR) return;
logger.error(msg);
},
fatal: (msg) => {
if (level <= levels.FATAL) return;
logger.fatal(msg);
},
// Custom loggers
parameters: (parameters) => {
if (level <= levels.TRACE) return;
logger.debug({ parameters }, 'Function Parameters');
},
functionName: (name) => {
if (level <= levels.TRACE) return;
logger.debug(`EXECUTING: ${name}`);
},
flow: (flow) => {
if (level <= levels.INFO) return;
logger.debug(`BEGIN FLOW: ${flow}`);
},
variable: ({ name, value }) => {
if (level <= levels.DEBUG) return;
// Check if the variable name matches any of the redact patterns and redact the value
let sanitizedValue = value;
for (const pattern of redactPatterns) {
if (pattern.test(name)) {
sanitizedValue = '***';
break;
}
}
logger.debug({ variable: { name, value: sanitizedValue } }, `VARIABLE ${name}`);
},
request: () => (req, res, next) => {
if (level < levels.DEBUG) return next();
logger.debug({ query: req.query, body: req.body }, `Hit URL ${req.url} with following`);
return next();
}
}
};

View File

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

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

@@ -0,0 +1,40 @@
const models = [
'text-davinci-003',
'text-davinci-002',
'text-davinci-001',
'text-curie-001',
'text-babbage-001',
'text-ada-001',
'davinci',
'curie',
'babbage',
'ada',
'code-davinci-002',
'code-davinci-001',
'code-cushman-002',
'code-cushman-001',
'davinci-codex',
'cushman-codex',
'text-davinci-edit-001',
'code-davinci-edit-001',
'text-embedding-ada-002',
'text-similarity-davinci-001',
'text-similarity-curie-001',
'text-similarity-babbage-001',
'text-similarity-ada-001',
'text-search-davinci-doc-001',
'text-search-curie-doc-001',
'text-search-babbage-doc-001',
'text-search-ada-doc-001',
'code-search-babbage-code-001',
'code-search-ada-code-001',
'gpt2',
'gpt-4',
'gpt-4-0314',
'gpt-4-32k',
'gpt-4-32k-0314',
'gpt-3.5-turbo',
'gpt-3.5-turbo-0301'
];
module.exports = new Set(models);

View File

@@ -1,23 +0,0 @@
{
/*
a preset is a set of plugins used to support particular language features.
The two presets Babel uses by default: es2015, react
*/
"presets": [
"@babel/preset-env", //compiling ES2015+ syntax
"@babel/preset-react" //for react
],
/*
Babel's code transformations are enabled by applying plugins (or presets) to your configuration file.
*/
"plugins": [
"@babel/plugin-transform-runtime",
[
"babel-plugin-root-import",
{
"rootPathPrefix": "~/",
"rootPathSuffix": "./src"
}
]
]
}

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

23
client/babel.config.cjs Normal file
View File

@@ -0,0 +1,23 @@
module.exports = {
presets: [
["@babel/preset-env", { "targets": { "node": "current" } }], //compiling ES2015+ syntax
['@babel/preset-react', {runtime: 'automatic'}],
"@babel/preset-typescript"
],
/*
Babel's code transformations are enabled by applying plugins (or presets) to your configuration file.
*/
plugins: [
"@babel/plugin-transform-runtime",
'babel-plugin-transform-import-meta',
'babel-plugin-transform-vite-meta-env',
'babel-plugin-replace-ts-export-assignment',
[
"babel-plugin-root-import",
{
"rootPathPrefix": "~/",
"rootPathSuffix": "./src"
}
]
]
}

12
client/env.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_SERVER_URL_DEV: string;
readonly VITE_SERVER_URL_PROD: string;
readonly VITE_SHOW_GOOGLE_LOGIN_OPTION: string;
readonly VITE_CLIENT_URL_DEV: string;
readonly VITE_CLIENT_URL_PROD: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

47
client/jest.config.cjs Normal file
View File

@@ -0,0 +1,47 @@
module.exports = {
roots: ['<rootDir>/src'],
testEnvironment: 'jsdom',
testEnvironmentOptions: {
url: 'http://localhost:3080',
},
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!<rootDir>/node_modules/',
'!src/**/*.css.d.ts',
'!src/**/*.d.ts',
],
coveragePathIgnorePatterns: [
'<rootDir>/node_modules/',
'<rootDir>/test/setupTests.js',
],
// Todo: Add coverageThreshold once we have enough coverage
// Note: eventually we want to have these values set to 80%
// coverageThreshold: {
// global: {
// functions: 9,
// lines: 40,
// statements: 40,
// branches: 12,
// },
// },
moduleNameMapper: {
'\\.(css)$': 'identity-obj-proxy',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'jest-file-loader',
'layout-test-utils': '<rootDir>/test/layout-test-utils',
'^@src/(.*)$': '<rootDir>/src/$1',
'^modules/(.*)$': '<rootDir>/src/modules/$1',
},
restoreMocks: true,
testResultsProcessor: 'jest-junit',
coverageReporters: ['text', 'cobertura', 'lcov'],
transform: {
'\\.[jt]sx?$': 'babel-jest',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'jest-file-loader',
},
preset: 'ts-jest',
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect', '<rootDir>/test/setupTests.js'],
clearMocks: true,
};

7
client/junit.xml Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="jest tests" tests="1" failures="0" errors="0" time="0.704">
<testsuite name="undefined" errors="0" failures="0" skipped="0" timestamp="2023-05-22T20:54:42" time="0.541" tests="1">
<testcase classname=" renders login form" name=" renders login form" time="0.035">
</testcase>
</testsuite>
</testsuites>

19932
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,13 @@
{
"name": "chatgpt-clone",
"version": "0.4.1",
"name": "chat-frontend",
"version": "0.4.7",
"description": "",
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite",
"preview-prod": "vite preview"
"preview-prod": "vite preview",
"test": "jest --watch",
"test:ci": "jest --ci"
},
"repository": {
"type": "git",
@@ -31,35 +32,37 @@
"@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",
"@tailwindcss/forms": "^0.5.3",
"@tanstack/react-query": "^4.28.0",
"@types/jest": "^29.5.0",
"@types/node": "^18.15.10",
"@types/node": "^20.2.3",
"@types/react": "^18.0.30",
"@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",
"esbuild": "0.17.19",
"export-from-json": "^1.7.2",
"filenamify": "^5.1.1",
"filenamify": "^6.0.0",
"html2canvas": "^1.4.1",
"lodash": "^4.17.21",
"lucide-react": "^0.113.0",
"lucide-react": "^0.220.0",
"pino": "^8.12.1",
"rc-input-number": "^7.4.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.43.9",
"react-lazy-load": "^4.0.1",
"react-markdown": "^8.0.6",
"react-router-dom": "^6.9.0",
"react-router-dom": "^6.11.2",
"react-string-replace": "^1.1.0",
"react-textarea-autosize": "^8.4.0",
"react-transition-group": "^4.4.5",
@@ -78,41 +81,48 @@
},
"devDependencies": {
"@babel/cli": "^7.20.7",
"@babel/core": "^7.20.12",
"@babel/core": "^7.21.8",
"@babel/eslint-parser": "^7.19.1",
"@babel/plugin-transform-runtime": "^7.19.6",
"@babel/preset-env": "^7.20.2",
"@babel/plugin-transform-runtime": "^7.21.4",
"@babel/preset-env": "^7.21.5",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.0",
"@babel/runtime": "^7.20.13",
"@tanstack/react-query-devtools": "^4.29.0",
"@types/jest": "^29.5.0",
"@types/node": "^18.15.10",
"@types/react": "^18.0.30",
"@testing-library/dom": "^9.3.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.5.1",
"@types/node": "^18.16.13",
"@types/react": "^18.2.6",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^3.1.0",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.13",
"babel-jest": "^29.5.0",
"babel-loader": "^9.1.2",
"babel-plugin-replace-ts-export-assignment": "^0.0.2",
"babel-plugin-root-import": "^6.6.0",
"babel-plugin-transform-import-meta": "^2.2.0",
"babel-plugin-transform-vite-meta-env": "^1.0.3",
"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",
"jest": "^29.5.0",
"jest-canvas-mock": "^2.5.1",
"jest-environment-jsdom": "^29.5.0",
"jest-file-loader": "^1.0.3",
"jest-junit": "^16.0.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-jest": "^29.1.0",
"ts-loader": "^9.4.2",
"typescript": "^4.9.5",
"typescript": "^5.0.4",
"vite": "^4.2.1",
"vite-plugin-html": "^3.2.0"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

@@ -0,0 +1,8 @@
import { render } from 'layout-test-utils';
import Login from '../Login';
test('renders login form', () => {
const { getByLabelText } = render(<Login />);
expect(getByLabelText(/email/i)).toBeInTheDocument();
expect(getByLabelText(/password/i)).toBeInTheDocument();
});

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);
}
@@ -86,14 +86,11 @@ export default function Conversation({ conversation, retainView }) {
if (currentConversation?.conversationId !== conversationId) {
aProps.className =
'group relative flex cursor-pointer items-center gap-3 break-all rounded-md py-3 px-3 hover:bg-[#2A2B32] hover:pr-4';
'group relative flex cursor-pointer items-center gap-3 break-all rounded-md py-3 px-3 hover:bg-gray-800 hover:pr-4';
}
return (
<a
onClick={() => clickHandler()}
{...aProps}
>
<a onClick={() => clickHandler()} {...aProps}>
<ConvoIcon />
<div className="relative max-h-5 flex-1 overflow-hidden text-ellipsis break-all">
{renaming === true ? (
@@ -102,7 +99,7 @@ export default function Conversation({ conversation, retainView }) {
type="text"
className="m-0 mr-0 w-full border border-blue-500 bg-transparent p-0 text-sm leading-tight outline-none"
value={titleInput}
onChange={e => setTitleInput(e.target.value)}
onChange={(e) => setTitleInput(e.target.value)}
onBlur={onRename}
onKeyDown={handleKeyDown}
/>
@@ -126,7 +123,7 @@ export default function Conversation({ conversation, retainView }) {
/>
</div>
) : (
<div className="absolute inset-y-0 right-0 z-10 w-8 bg-gradient-to-l from-gray-900 group-hover:from-[#2A2B32]" />
<div className="absolute inset-y-0 right-0 z-10 w-8 rounded-r-md bg-gradient-to-l from-gray-900 group-hover:from-gray-700/70" />
)}
</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={

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