Compare commits

...

26 Commits

Author SHA1 Message Date
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
55 changed files with 9082 additions and 14998 deletions

View File

@@ -10,6 +10,7 @@ module.exports = {
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
"plugin:jest/recommended",
'prettier'
],
parser: '@typescript-eslint/parser',
@@ -64,7 +65,7 @@ module.exports = {
{
files: ['rollup.config.js', '.eslintrc.js', 'jest.config.js'],
env: {
node: true
node: true,
}
},
{
@@ -95,7 +96,7 @@ module.exports = {
parserOptions: {
project: './client/tsconfig.json'
},
plugins: ['@typescript-eslint/eslint-plugin'],
plugins: ['@typescript-eslint/eslint-plugin', 'jest'],
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended'

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

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

View File

@@ -112,21 +112,11 @@
* [Documentation Guidelines](documents/contributions/documentation_guidelines.md)
* [Code Standards and Conventions](documents/contributions/coding_conventions.md)
* [Testing](documents/contributions/testing.md)
* [Pull Request Template](documents/contributions/pull_request_template.md)
* [Security](SECURITY.md)
* [Contributors](CONTRIBUTORS.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>
##
### [Alternative Documentation](https://chatgpt-clone.gitbook.io/chatgpt-clone-docs/get-started/docker)
##

View File

@@ -9,7 +9,7 @@ If you discover a security vulnerability within our project, please follow these
### Communication channels
- **Option 1: GitHub Security Advisory System**: We encourage you to use GitHub's Security Advisory system to report any security vulnerabilities you find. This allows us to receive vulnerability reports directly through GitHub. You can find more information on how to submit a security advisory report in the [GitHub Security Advisories documentation](https://docs.github.com/en/code-security/getting-started-with-security-vulnerability-alerts/about-github-security-advisories).
- **Option 2: Github issues**: You can initiate first contact via Github Issues. **Please note that initial contact through Discord should not include any sensitive details.**
- **Option 3: Discord Server**: You can join our [Discord community](https://discord.gg/NGaa9RPCft) and initiate first contact in the `#issues` channel. **Please note that initial contact through Discord should not include any sensitive details.**
- **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._
@@ -24,7 +24,7 @@ We will make every effort to acknowledge your report within 72 hours and keep yo
## Security Updates and Patching
We are committed to maintaining the security of our open-source project named ChatGPT-Clone and promptly addressing any identified vulnerabilities. To ensure the security of our project, we follow these practices:
- We prioritize security updates for the current major release of our software.
- We actively monitor the GitHub Security Advisory system and the `#security-reports` channel on Discord for any vulnerability reports.
- We 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.
@@ -45,3 +45,11 @@ We would like to express our gratitude to the security researchers and community
## 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.
##
## [Go Back to ReadMe](README.md)
**Reference**
- https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html

View File

@@ -59,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
# As of 5/23/23, to use Bing, you will need your full cookie string from bing.com. Use dev tools or an extension while
# logged into the site to view it in your network request Cookie header value. For full instructions, see my comment here:
# https://github.com/waylaidwanderer/node-chatgpt-api/issues/378#issuecomment-1559868368
# 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"

View File

@@ -23,10 +23,10 @@ const askBing = async ({
const bingAIClient = new BingAIClient({
// "_U" cookie from bing.com
userToken:
process.env.BINGAI_TOKEN == 'user_provided' ? token : process.env.BINGAI_TOKEN ?? null,
// userToken:
// process.env.BINGAI_TOKEN == 'user_provided' ? token : process.env.BINGAI_TOKEN ?? null,
// If the above doesn't work, provide all your cookies as a string instead
// cookies: '',
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,6 +8,7 @@ const browserClient = async ({
model,
token,
onProgress,
onEventMessage,
abortController,
userId
}) => {
@@ -30,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

@@ -2,6 +2,7 @@ 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 ({
@@ -26,9 +27,8 @@ const askClient = async ({
};
const azure = process.env.AZURE_OPENAI_API_KEY ? true : false;
if (promptPrefix == null) {
promptText = 'You are ChatGPT, a large language model trained by OpenAI.';
} else {
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
@@ -68,25 +68,26 @@ const askClient = async ({
...(parentMessageId && conversationId ? { parentMessageId, conversationId } : {})
};
const enc = encoding_for_model(model);
const text_tokens = enc.encode(text);
const prompt_tokens = enc.encode(promptText);
// console.log("Prompt tokens = ", prompt_tokens.length);
// console.log("Message Tokens = ", text_tokens.length);
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 });
// return res;
// create a new response object that includes the token counts
const newRes = {
...res,
usage: {
prompt_tokens: prompt_tokens.length,
completion_tokens: text_tokens.length,
total_tokens: prompt_tokens.length + text_tokens.length
}
};
return newRes;
try {
usage.completion_tokens = (enc.encode(res.response)).length;
usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
res.usage = usage;
} catch (e) {
console.log('Error encoding response text', e);
}
return res;
};
module.exports = { askClient };

View File

@@ -266,7 +266,7 @@ class GoogleAgent {
const user = opts.user || null;
const conversationId = opts.conversationId || crypto.randomUUID();
const parentMessageId = opts.parentMessageId || '00000000-0000-0000-0000-000000000000';
const userMessageId = crypto.randomUUID();
const userMessageId = opts.overrideParentMessageId || crypto.randomUUID();
const responseMessageId = crypto.randomUUID();
const messages = await this.loadHistory(conversationId, this.options?.parentMessageId);

View File

@@ -24,6 +24,10 @@ const convoSchema = mongoose.Schema(
examples: [{ type: mongoose.Schema.Types.Mixed }],
...conversationPreset,
// for bingAI only
bingConversationId: {
type: String,
default: null
},
jailbreakConversationId: {
type: String,
default: null

View File

@@ -23,14 +23,12 @@
"@keyv/mongo": "^2.1.8",
"@waylaidwanderer/chatgpt-api": "^1.36.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",

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,7 +152,7 @@ const ask = async ({
const newConversationId = endpointOption?.jailbreak
? response.jailbreakConversationId
: response.conversationId || conversationId;
const newUserMassageId =
const newUserMessageId =
response.parentMessageId || response.details.requestId || userMessageId;
const newResponseMessageId = response.messageId || response.details.messageId;
@@ -156,10 +161,11 @@ const ask = async ({
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:
@@ -173,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;
@@ -210,20 +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
newMessageId: newUserMessageId
});
userMessageId = newUserMassageId;
userMessageId = newUserMessageId;
sendMessage(res, {
title: await getConvoTitle(req.user.id, 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);
@@ -212,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

@@ -1,5 +1,6 @@
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');
@@ -7,7 +8,7 @@ const { handleError, sendMessage, createOnProgress } = require('./handlers');
const requireJwtAuth = require('../../../middleware/requireJwtAuth');
router.post('/', requireJwtAuth, async (req, res) => {
const { endpoint, text, parentMessageId, conversationId } = req.body;
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' });
@@ -31,6 +32,8 @@ router.post('/', requireJwtAuth, async (req, res) => {
return handleError(res, { text: `Illegal request: model` });
}
const conversationId = oldConversationId || crypto.randomUUID();
// eslint-disable-next-line no-use-before-define
return await ask({
text,
@@ -54,6 +57,7 @@ const ask = async ({ text, endpointOption, parentMessageId = null, conversationI
let userMessageId;
let responseMessageId;
let lastSavedTimestamp = 0;
const { overrideParentMessageId = null } = req.body;
try {
const getIds = (data) => {
@@ -63,6 +67,8 @@ const ask = async ({ text, endpointOption, parentMessageId = null, conversationI
if (!conversationId) {
conversationId = data.conversationId;
}
sendMessage(res, { message: userMessage, created: true });
};
const { onProgress: progressCallback } = createOnProgress({
@@ -74,7 +80,7 @@ const ask = async ({ text, endpointOption, parentMessageId = null, conversationI
messageId: responseMessageId,
sender: 'PaLM2',
conversationId,
parentMessageId: userMessageId,
parentMessageId: overrideParentMessageId || userMessageId,
text: partialText,
unfinished: true,
cancelled: false,
@@ -113,12 +119,28 @@ const ask = async ({ text, endpointOption, parentMessageId = null, conversationI
let response = await client.sendMessage(text, {
getIds,
user: req.user.id,
parentMessageId,
conversationId,
onProgress: progressCallback.call(null, { res, text, parentMessageId: userMessageId }),
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),
@@ -132,7 +154,7 @@ const ask = async ({ text, endpointOption, parentMessageId = null, conversationI
if (parentMessageId == '00000000-0000-0000-0000-000000000000') {
const title = await titleConvo({ text, response });
await saveConvo(req.user.id, {
conversationId: conversationId,
conversationId,
title
});
}

View File

@@ -190,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
@@ -200,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,
@@ -234,16 +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
newMessageId: newUserMessageId
});
userMessageId = newUserMassageId;
userMessageId = newUserMessageId;
sendMessage(res, {
title: await getConvoTitle(req.user.id, conversationId),

View File

@@ -2,7 +2,7 @@ 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 { registerSchema } = require('../../strategies/validators');
const migrateDataToFirstUser = require('../../utils/migrateDataToFirstUser');
@@ -90,20 +90,13 @@ const registerUser = async (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;
@@ -127,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,
@@ -156,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 });

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

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>

View File

@@ -2,11 +2,12 @@
"name": "chat-frontend",
"version": "0.4.6",
"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",
@@ -38,7 +39,7 @@
"@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",
@@ -48,7 +49,7 @@
"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": "^6.0.0",
"html2canvas": "^1.4.1",
@@ -80,24 +81,38 @@
},
"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-plugin-jest": "^27.2.1",
"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",
@@ -105,6 +120,7 @@
"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": "^5.0.4",
"vite": "^4.2.1",

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

@@ -47,7 +47,7 @@ function Settings(props) {
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
<div className="grid w-full items-center gap-2">
<Label htmlFor="toneStyle-dropdown" className="text-left text-sm font-medium">
Tone Style <small className="opacity-40">(default: fast)</small>
Tone Style <small className="opacity-40">(default: creative)</small>
</Label>
<SelectDropDown
id="toneStyle-dropdown"
@@ -112,7 +112,7 @@ function Settings(props) {
<a
href="https://github.com/danny-avila/chatgpt-clone/blob/main/client/defaultSystemMessage.md"
target="_blank"
className="text-blue-500 transition-colors duration-200 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-500"
className="text-blue-500 transition-colors duration-200 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-500" rel="noreferrer"
>
System Message
</a>{' '}

View File

@@ -15,7 +15,7 @@ const alternateName = {
google: 'PaLM'
};
export default function ModelItem({ endpoint, value, onSelect }) {
export default function ModelItem({ endpoint, value }) {
const [setTokenDialogOpen, setSetTokenDialogOpen] = useState(false);
const endpointsConfig = useRecoilValue(store.endpointsConfig);
@@ -33,6 +33,7 @@ export default function ModelItem({ endpoint, value, onSelect }) {
<>
<DropdownMenuRadioItem
value={value}
id={endpoint}
className="group dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
>
{icon}

View File

@@ -126,6 +126,7 @@ export default function NewConversationMenu() {
<DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
<DropdownMenuTrigger asChild>
<Button
id="new-conversation-menu"
variant="outline"
className={`group relative mb-[-12px] ml-0 mt-[-8px] items-center rounded-md border-0 p-1 outline-none focus:ring-0 focus:ring-offset-0 dark:data-[state=open]:bg-opacity-50 md:left-1 md:ml-[-12px] md:pl-1`}
>

View File

@@ -1,9 +1,6 @@
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import DialogTemplate from '../../ui/DialogTemplate';
import { Dialog } from '../../ui/Dialog.tsx';
import { Input } from '../../ui/Input.tsx';
import { Label } from '../../ui/Label.tsx';
import { cn } from '~/utils/';
import * as Checkbox from '@radix-ui/react-checkbox';
import { CheckIcon } from '@radix-ui/react-icons';
import FileUpload from '../NewConversationMenu/FileUpload';
@@ -24,9 +21,6 @@ const SetTokenDialog = ({ open, onOpenChange, endpoint }) => {
const [showPanel, setShowPanel] = useState(false);
const { getToken, saveToken } = store.useToken(endpoint);
const defaultTextProps =
'rounded-md border border-gray-300 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.10)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-400 dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0';
const submit = () => {
saveToken(token);
onOpenChange(false);
@@ -49,13 +43,21 @@ const SetTokenDialog = ({ open, onOpenChange, endpoint }) => {
const helpText = {
bingAI: (
<small className="break-all text-gray-600">
The Bing Access Token is the "_U" cookie from bing.com. Use dev tools or an extension while
logged into the site to view it.
{`As of 5/23/23, to use Bing, you will need your full cookie string from bing.com. Use dev tools or an extension while
logged into the site to view it in your network request Cookie header value. For full instructions, see my `}
<a
target="_blank"
href="https://github.com/waylaidwanderer/node-chatgpt-api/issues/378#issuecomment-1559868368"
rel="noreferrer"
className="text-blue-600 underline"
>
comment here
</a>
</small>
),
chatGPTBrowser: (
<small className="break-all text-gray-600">
To get your Access token For ChatGPT 'Free Version', login to{' '}
{`To get your Access token For ChatGPT 'Free Version', login to `}
<a
target="_blank"
href="https://chat.openai.com"
@@ -96,8 +98,8 @@ const SetTokenDialog = ({ open, onOpenChange, endpoint }) => {
>
Create a Service Account
</a>
. Make sure to click 'Create and Continue' to give at least the 'Vertex AI User' role.
Lastly, create a JSON key to import here.
{`. Make sure to click 'Create and Continue' to give at least the 'Vertex AI User' role.
Lastly, create a JSON key to import here.`}
</small>
)
};

View File

@@ -5,7 +5,6 @@ import OpenAIOptions from './OpenAIOptions';
import ChatGPTOptions from './ChatGPTOptions';
import BingAIOptions from './BingAIOptions';
import GoogleOptions from './GoogleOptions';
// import BingStyles from './BingStyles';
import NewConversationMenu from './NewConversationMenu';
import AdjustToneButton from './AdjustToneButton';
import Footer from './Footer';
@@ -21,7 +20,6 @@ export default function TextChat({ isSearchView = false }) {
const conversation = useRecoilValue(store.conversation);
const latestMessage = useRecoilValue(store.latestMessage);
const [text, setText] = useRecoilState(store.text);
// const [text, setText] = useState('');
const endpointsConfig = useRecoilValue(store.endpointsConfig);
const isSubmitting = useRecoilValue(store.isSubmitting);
@@ -30,8 +28,6 @@ export default function TextChat({ isSearchView = false }) {
const disabled = false;
const { ask, stopGenerating } = useMessageHandler();
// const bingStylesRef = useRef(null);
const [showBingToneSetting, setShowBingToneSetting] = useState(false);
const isNotAppendable = latestMessage?.unfinished & !isSubmitting || latestMessage?.error;
@@ -39,25 +35,15 @@ export default function TextChat({ isSearchView = false }) {
// auto focus to input, when enter a conversation.
useEffect(() => {
if (conversation?.conversationId !== 'search') inputRef.current?.focus();
// setText('');
}, [conversation?.conversationId]);
// // controls the height of Bing tone style tabs
// useEffect(() => {
// if (!inputRef.current) {
// return; // wait for the ref to be available
// }
// const resizeObserver = new ResizeObserver(() => {
// const newHeight = inputRef.current.clientHeight;
// if (newHeight >= 24) {
// // 24 is the default height of the input
// bingStylesRef.current.style.bottom = 15 + newHeight + 'px';
// }
// });
// resizeObserver.observe(inputRef.current);
// return () => resizeObserver.disconnect();
// }, [inputRef]);
useEffect(() => {
const timeoutId = setTimeout(() => {
inputRef.current?.focus();
}, 100);
return () => clearTimeout(timeoutId);
}, [isSubmitting]);
const submitMessage = () => {
ask({ text });
@@ -156,6 +142,8 @@ export default function TextChat({ isSearchView = false }) {
>
<NewConversationMenu />
<TextareaAutosize
// set test id for e2e testing
data-testid="text-input"
tabIndex="0"
autoFocus
ref={inputRef}

View File

@@ -1,8 +1,7 @@
import { useEffect, useState } from 'react';
import { useRecoilValue, useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
import { useEffect } from 'react';
import { useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil';
import { SSE } from '~/data-provider/sse.mjs';
import createPayload from '~/data-provider/createPayload';
import { useAbortRequestWithMessage } from '~/data-provider';
import store from '~/store';
import { useAuthContext } from '~/hooks/AuthContext';
@@ -117,8 +116,11 @@ export default function MessageHandler() {
const { requestMessage, responseMessage, conversation } = data;
// update the messages
if (isRegenerate) setMessages([...messages, responseMessage]);
else setMessages([...messages, requestMessage, responseMessage]);
if (isRegenerate) {
setMessages([...messages, responseMessage]);
} else {
setMessages([...messages, requestMessage, responseMessage]);
}
setIsSubmitting(false);
// refresh title

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { cn } from '~/utils/';
import Clipboard from '../svg/Clipboard';
import CheckMark from '../svg/CheckMark';
import EditIcon from '../svg/EditIcon';
@@ -13,14 +14,13 @@ export default function HoverButtons({
message,
regenerate
}) {
const { endpoint, jailbreak = false } = conversation;
const { endpoint } = conversation;
const [isCopied, setIsCopied] = React.useState(false);
const branchingSupported =
// azureOpenAI, openAI, chatGPTBrowser support branching, so edit enabled
!!['azureOpenAI', 'openAI', 'chatGPTBrowser', 'google'].find((e) => e === endpoint) ||
// Sydney in bingAI supports branching, so edit enabled
(endpoint === 'bingAI' && jailbreak);
// azureOpenAI, openAI, chatGPTBrowser support branching, so edit enabled // 5/21/23: Bing is allowing editing and Message regenerating
!!['azureOpenAI', 'openAI', 'chatGPTBrowser', 'google', 'bingAI'].find((e) => e === endpoint);
// Sydney in bingAI supports branching, so edit enabled
const editEnabled =
!message?.error &&
@@ -30,7 +30,7 @@ export default function HoverButtons({
branchingSupported;
// for now, once branching is supported, regerate will be enabled
const regenerateEnabled =
let regenerateEnabled =
// !message?.error &&
!message?.isCreatedByUser &&
!message?.searchResult &&
@@ -53,7 +53,7 @@ export default function HoverButtons({
) : null}
{regenerateEnabled ? (
<button
className="hover-button rounded-md p-1 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible"
className="hover-button active rounded-md p-1 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible"
onClick={regenerate}
type="button"
title="regenerate"
@@ -64,7 +64,10 @@ export default function HoverButtons({
) : null}
<button
className="hover-button rounded-md p-1 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible"
className={cn(
'hover-button rounded-md p-1 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible',
message?.isCreatedByUser ? '' : 'active'
)}
onClick={() => copyToClipboard(setIsCopied)}
type="button"
title={isCopied ? 'Copied to clipboard' : 'Copy to clipboard'}

View File

@@ -16,6 +16,15 @@ export type TExample = {
output: string;
};
export enum EModelEndpoint {
azureOpenAI = 'azureOpenAI',
openAI = 'openAI',
bingAI = 'bingAI',
chatGPT = 'chatGPT',
chatGPTBrowser = 'chatGPTBrowser',
google = 'google'
}
export type TSubmission = {
clientId?: string;
context?: string;
@@ -42,15 +51,6 @@ export type TSubmission = {
frequence_penalty?: number;
};
export enum EModelEndpoint {
azureOpenAI = 'azureOpenAI',
openAI = 'openAI',
bingAI = 'bingAI',
chatGPT = 'chatGPT',
chatGPTBrowser = 'chatGPTBrowser',
google = 'google'
}
export type TConversation = {
conversationId: string;
title: string;
@@ -157,7 +157,7 @@ export type TSearchResults = {
pageNumber: string;
pageSize: string | number;
pages: string | number;
filter: {};
filter: object;
};
export type TEndpoints = {
@@ -175,11 +175,11 @@ export type TUpdateTokenCountResponse = {
count: number;
};
export type TMessageTreeNode = {};
export type TMessageTreeNode = object;
export type TSearchMessage = {};
export type TSearchMessage = object;
export type TSearchMessageTreeNode = {};
export type TSearchMessageTreeNode = object;
export type TRegisterUser = {
name: string;

View File

@@ -1,40 +1,8 @@
.switch-container {
display: none;
}
.switch-result {
display: block !important;
visibility: visible;
}
/* .sibling-switch {
left: 114px;
top: unset;
bottom: 4px;
visibility: visible;
z-index: 2;
} */
.sibling-switch {
display: none;
}
.hover-button {
.hover-button.active {
display: block;
visibility: visible;
}
.input-panel-button {
border: 0;
}
.input-panel-button svg {
width: 16px;
height: 16px;
}
.input-panel {
}
.nav-close-button {
display: block;
position: absolute;
@@ -96,3 +64,47 @@
position: fixed;
}
}
@media (min-width: 1024px) {
.switch-container {
display: none;
}
}
.switch-result {
display: block !important;
visibility: visible;
}
@media (max-width: 1024px) {
/* .sibling-switch {
left: 114px;
top: unset;
bottom: 4px;
visibility: visible;
z-index: 2;
} */
.sibling-switch {
display: none;
}
.hover-button {
display: block;
visibility: visible;
}
}
@media (max-width: 767px) {
.input-panel-button {
border: 0;
}
.input-panel-button svg {
width: 16px;
height: 16px;
}
.input-panel {
}
}

View File

@@ -36,7 +36,7 @@ const cleanupPreset = ({ preset: _preset, endpointsConfig = {} }) => {
jailbreak: _preset?.jailbreak ?? false,
context: _preset?.context ?? null,
systemMessage: _preset?.systemMessage ?? null,
toneStyle: _preset?.toneStyle ?? 'fast',
toneStyle: _preset?.toneStyle ?? 'creative',
title: _preset?.title ?? 'New Preset'
};
} else if (endpoint === 'chatGPTBrowser') {

View File

@@ -48,7 +48,7 @@ const buildDefaultConversation = ({
jailbreak: lastConversationSetup?.jailbreak ?? false,
context: lastConversationSetup?.context ?? null,
systemMessage: lastConversationSetup?.systemMessage ?? null,
toneStyle: lastConversationSetup?.toneStyle ?? 'fast',
toneStyle: lastConversationSetup?.toneStyle ?? 'creative',
jailbreakConversationId: lastConversationSetup?.jailbreakConversationId ?? null,
conversationSignature: null,
clientId: null,
@@ -80,7 +80,7 @@ const buildDefaultConversation = ({
return conversation;
};
const getDefaultConversation = ({ conversation, prevConversation, endpointsConfig, preset }) => {
const getDefaultConversation = ({ conversation, endpointsConfig, preset }) => {
const { endpoint: targetEndpoint } = preset || {};
if (targetEndpoint) {
@@ -123,7 +123,9 @@ const getDefaultConversation = ({ conversation, prevConversation, endpointsConfi
conversation = buildDefaultConversation({ conversation, endpoint, endpointsConfig });
return conversation;
}
} catch (error) {}
} catch (error) {
console.error(error);
}
// if anything happens, reset to default model

View File

@@ -50,7 +50,7 @@ const useMessageHandler = () => {
currentConversation?.model ??
endpointsConfig[endpoint]?.availableModels?.[0] ??
'chat-bison',
chatGptLabel: currentConversation?.chatGptLabel ?? null,
modelLabel: currentConversation?.modelLabel ?? null,
promptPrefix: currentConversation?.promptPrefix ?? null,
examples: currentConversation?.examples ?? [
{ input: { content: '' }, output: { content: '' } }
@@ -68,7 +68,7 @@ const useMessageHandler = () => {
jailbreak: currentConversation?.jailbreak ?? false,
systemMessage: currentConversation?.systemMessage ?? null,
context: currentConversation?.context ?? null,
toneStyle: currentConversation?.toneStyle ?? 'fast',
toneStyle: currentConversation?.toneStyle ?? 'creative',
jailbreakConversationId: currentConversation?.jailbreakConversationId ?? null,
conversationSignature: currentConversation?.conversationSignature ?? null,
clientId: currentConversation?.clientId ?? null,

View File

@@ -0,0 +1,21 @@
import { render as rtlRender } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AuthContextProvider } from '~/hooks/AuthContext';
import { BrowserRouter as Router } from 'react-router-dom';
const client = new QueryClient();
function renderWithProvidersWrapper(ui, { ...options } = {}) {
function Wrapper({ children }) {
return (
<QueryClientProvider client={client}>
<Router>
<AuthContextProvider>{children}</AuthContextProvider>
</Router>
</QueryClientProvider>
);
}
return rtlRender(ui, { wrapper: Wrapper, ...options });
}
export * from '@testing-library/react';
export { renderWithProvidersWrapper as render };

18
client/test/setupTests.js Normal file
View File

@@ -0,0 +1,18 @@
/* This file is automatically executed before running tests
* https://create-react-app.dev/docs/running-tests/#initializing-test-environment
*/
// react-testing-library renders your components to document.body,
// this adds jest-dom's custom assertions
// https://github.com/testing-library/jest-dom#table-of-contents
import '@testing-library/jest-dom';
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';
// Mock canvas when run unit test cases with jest.
// 'react-lottie' uses canvas
import 'jest-canvas-mock';

View File

@@ -2,14 +2,10 @@
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": [
"DOM",
"DOM.Iterable",
"ESNext"
],
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": true,
"skipLibCheck": false,
"esModuleInterop": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": false,
@@ -19,13 +15,20 @@
"isolatedModules": true,
"noImplicitAny": false,
"noEmit": false,
"declaration": true,
"declarationDir": "./types",
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"~/*": ["./src/*"]
}
},
"include": [
"src"
],
}
"types": ["node", "jest", "@testing-library/jest-dom"],
"exclude": ["node_modules"],
"include": ["src/**/*", "setupTests.js", "vite.config.ts", "env.d.ts"],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -14,10 +14,6 @@ export default defineConfig({
target: 'http://localhost:3080',
changeOrigin: true
},
'/auth': {
target: 'http://localhost:3080',
changeOrigin: true
},
'/oauth': {
target: 'http://localhost:3080',
changeOrigin: true

View File

@@ -43,7 +43,7 @@ Contains the logic for each route, including calling the appropriate service fun
- Create a separate controller file for each route to handle the request/response logic.
- Name controller files using the PascalCase convention and append "Controller" to the file name (e.g., UserController.js).
- Use controller methods to encapsulate logic related to the route handling.
- eep controllers thin by delegating complex operations to service or model files.
- Keep controllers thin by delegating complex operations to service or model files.
#### Services
@@ -101,4 +101,4 @@ Use the conventions found in the `data-provider` directory for handling data ser
### State Management
Use [Recoil](https://recoiljs.org/) for state management, but *DO NOT pollute the global state with unnecessary data*. Instead, use local state or props for data that is only used within a component or passed down from parent to child.

5
documents/dev/README.md Normal file
View File

@@ -0,0 +1,5 @@
This directory contains files used for developer work
- Dockerfile-app: used to build the DockerHub image
- eslintrc-stripped.js: alternate linting rules, used in development
- meilisearch.yml: Dockerfile for building meilisearch image independently from project

View File

@@ -2,21 +2,38 @@
Thanks to @heathriel!
##
**Install the prerequisites**:
**Recommended - [Docker Install](docker_install.md)**
## **Install the prerequisites**:
- Install Homebrew (if not already installed) by following the instructions on https://brew.sh/
- Install Node.js and npm by running `brew install node`
- Install MongoDB (if not using Docker) by running `brew tap mongodb/brew` and `brew install mongodb-community`
- Install Docker (optional) by following the instructions on https://docs.docker.com/desktop/mac/install/
- Obtain an OpenAI API key, BingAI and ChatGPT access tokens as described in the original instructions
-
**Instructions:**
- **Create a MongoDB database**
- Navigate to https://www.mongodb.com/ and Sign In or Create an account
- Create a new project
- Build a Database using the free plan and name the cluster (example: chatgpt-clone)
- Use the "Username and Password" method for authentication
- Add your current IP to the access list
- Then in the Database Deployment tab click on Connect
- In "Choose a connection method" select "Connect your application"
- Driver = Node.js / Version = 4.1 or later
- Copy the connection string and save it somewhere(you will need it later)
## **Instructions:**
- Open Terminal and clone the repository by running git clone https://github.com/danny-avila/chatgpt-clone.git
- Change into the cloned directory by running cd chatgpt-clone
- If using MongoDB Atlas, remove &w=majority from the default connection string
Follow the instructions for setting up proxies, access tokens, and user system:
**Access Tokens:**
### **Access Tokens:**
**Get your OpenAI API key**
- here: https://platform.openai.com/account/api-keys and save it somewhere safe (you will need it later)
**ChatGPT Free Instructions:**
@@ -31,33 +48,13 @@ Follow the instructions for setting up proxies, access tokens, and user system:
- Expand the "Cookies" section under "Storage".
- Copy the value of the "_U" cookie and save it somewhere. You'll need it later.
**Set up proxy in the local environment (for Mac):**
**Option 1: Set system-level environment variable**
- Open Terminal and run export PROXY="http://127.0.0.1:7890"
- Change http://127.0.0.1:7890 to your proxy server
**Option 2: Set in .env file**
- Open the .env file in the api directory with a text editor
- Add PROXY="http://127.0.0.1:7890" to the file
- Change http://127.0.0.1:7890 to your proxy server
**Set up proxy in the Docker environment (for Mac):**
- Open the docker-compose.yml file with a text editor
- Under services, find the api section, and then locate the environment section
- Add the line - "PROXY=http://127.0.0.1:7890" under the environment section
- Change http://127.0.0.1:7890 to your proxy server
## **Setup Instruction**
- Create a .env file in the api directory by running cp api/.env.example api/.env and edit the file with your preferred text editor, adding the required API keys, access tokens, and MongoDB connection string
- Run npm ci root directory `npm ci`
- Run npm ci from root directory `npm ci`
- Build the client by running `npm run frontend`
**Download MeiliSearch for macOS:**
**Download MeiliSearch for macOS (optional):**
- You can download the latest MeiliSearch binary for macOS from their GitHub releases page: https://github.com/meilisearch/MeiliSearch/releases. Look for the file named meilisearch-macos-amd64 (or the equivalent for your system architecture) and download it.
**Make the binary executable:**
@@ -85,10 +82,10 @@ MEILISEARCH_KEY=your_master_key_goes_here
- With MeiliSearch running and configured, the ChatGPT-Clone project should now have the Conversation search feature enabled.
- In the chatgpt-clone directory, start the application by running cd api && npm start
- In the chatgpt-clone directory, start the application by running `npm run backend`
Visit http://localhost:3080 (default port) & enjoy
**Optional but recommended:**
## **Optional but recommended:**
- Create a script to automate the starting process by creating a new file named start_chatgpt.sh in the chatgpt-clone directory and pasting the following code:
@@ -111,9 +108,12 @@ npm run backend
```
./start_chatgpt.sh
```
##
**Note:**
- To share within the network or serve as a public server, set HOST to 0.0.0.0 in the .env file.
## **Update**
- run `git pull` from the root dir
- Run npm ci from root directory `npm ci`
- Build the client by running `npm run frontend`
##

View File

@@ -1,22 +1,24 @@
# Windows Install
### Recommended:
### **[Docker](docker.md)**
or
### **[Automated Installer (Windows)](https://github.com/fuegovic/chatgpt-clone-local-installer)**
(Includes a Startup and Update Utility)
##
## Manual Installation
### Install the prerequisites on your machine
- **Download chatgpt-clone**
-
- Download the latest release here: https://github.com/danny-avila/chatgpt-clone/releases/
- Or by clicking on the green code button in the top of the page and selecting "Download ZIP"
- Or (Recommended if you have Git installed) pull the latest release from the main branch
- If you downloaded a zip file, extract the content in "C:/chatgpt-clone/"
- **IMPORTANT : If you install the files somewhere else modify the instructions accordingly**
- ** **Enable the Conversation search feature:**** (optional)
- **Enable the Conversation search feature:** (optional)
- Download MeiliSearch latest release from : https://github.com/meilisearch/meilisearch/releases
- Copy it to "C:/chatgpt-clone/"
@@ -81,25 +83,24 @@ To use the app:
- **Make a batch file to automate the starting process**
- Open a text editor
- Paste the following code in a new document
- The meilisearch executable needs to be at the root of the chatgpt-clone directory
- Put your MeiliSearch master key instead of "your_master_key_goes_here"
- Save the file as "C:/chatgpt-clone/chatgpt-clone.bat"
- you can make a shortcut of this batch file and put it anywhere
```
REM the meilisearch executable needs to be at the root of the chatgpt-clone directory
start "MeiliSearch" cmd /k "meilisearch --master-key your_master_key_goes_here
REM ↑↑↑ meilisearch is the name of the meilisearch executable, put your own master key there
start "ChatGPT-Clone" cmd /k "npm run backend"
REM this batch file goes at the root of the chatgpt-clone directory (C:/chatgpt-clone/)
```
### Update the app version
### **Update**
- run `git pull` from the root dir
- Run npm ci from root directory `npm ci`
- Build the client by running `npm run frontend`
If you update the chatgpt-clone project files, mannually redo the `npm ci` and `npm run frontend` steps
##

View File

@@ -1,41 +0,0 @@
---
name: Bug report
about: Create a report to help improve code's quality
title: "[bug] "
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,8 +0,0 @@
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---

View File

@@ -1,17 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[feature] "
labels: enhancement
assignees: ''
---
**Feature description**
...
**What you expect**
...
**Addition(s)**
...

View File

@@ -0,0 +1,45 @@
import { expect, test } from '@playwright/test';
const endpoints = ['google', 'openAI', 'azureOpenAI', 'bingAI', 'chatGPTBrowser', 'gptPlugins'];
test.describe('Messaging suite', () => {
let myBrowser;
test.beforeEach(async ({ browser }) => {
myBrowser = await browser.newContext({
storageState: 'e2e/auth.json'
});
});
test('textbox should be focused after receiving message', async () => {
test.setTimeout(120000);
const page = await myBrowser.newPage();
const message = 'hi';
const endpoint = endpoints[0];
await page.goto('http://localhost:3080/chat/new');
await page.locator('#new-conversation-menu').click();
await page.locator(`#${endpoint}`).click();
await page.locator('form').getByRole('textbox').click();
await page.locator('form').getByRole('textbox').fill(message);
const responsePromise = [
page.waitForResponse(async (response) => {
return response.url().includes(`/api/ask/${endpoint}`) && response.status() === 200;
}),
page.locator('form').getByRole('textbox').press('Enter')
];
const [response] = await Promise.all(responsePromise);
const responseBody = await response.body();
const messageSuccess = responseBody.includes(`"final":true`);
expect(messageSuccess).toBe(true);
// Check if textbox is focused
await page.waitForTimeout(250);
const isTextboxFocused = await page.evaluate(() => {
return document.activeElement === document.querySelector('[data-testid="text-input"]');
});
expect(isTextboxFocused).toBeTruthy();
});
});

23000
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@
"e2e:debug": "cross-env PWDEBUG=1 playwright test --config=e2e/playwright.config.js",
"e2e:report": "npx playwright show-report e2e/playwright-report",
"e2e:auth": "npx playwright codegen --save-storage=e2e/auth.json http://localhost:3080/",
"e2e:test-auth": "npx playwright codegen --load-storage=e2e/auth.json http://localhost:3080/",
"prepare": "husky install",
"format": "prettier-eslint --write \"{,!(node_modules)/**/}*.{js,jsx,ts,tsx}\""
},
@@ -35,7 +36,7 @@
"@typescript-eslint/parser": "^5.59.6",
"babel-eslint": "^10.1.0",
"cross-env": "^7.0.3",
"eslint": "^8.40.0",
"eslint": "^8.41.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-jest": "^27.2.1",