Compare commits

...

641 Commits

Author SHA1 Message Date
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
Danny Avila
03d871316a chore: bump version to 0.4.1 in package.json files (#222)
feat: update docker-compose.yml to use latest image from docker hub
2023-05-09 17:57:04 -04:00
David
4b94af0429 Update Message.js (#191)
Fixed Error handling, Code duplication and Naming conventions. Contact me for more information at: DavidTheDev#0166

Co-authored-by: Danny Avila <110412045+danny-avila@users.noreply.github.com>
2023-05-09 17:51:39 -04:00
Danny Avila
5dd9c11326 Feat: Add Azure support (#219)
feat(api): add support for Azure OpenAI API

- Add Azure OpenAI API environment variables to .env.example
- Modify chatgpt-client.js to use Azure OpenAI API if environment variables are present
- Modify askOpenAI.js to use arrow function syntax
- Modify handlers.js to add console.log statement for partial variable
2023-05-09 17:42:55 -04:00
Fuegovic
e2dc994b63 update documentation structure (#220)
* documentation refactor

* Update README.md

* Delete README.MD.md

* Delete LOCAL_INSTALL.md

* Rename LICENSE.MD.md to LICENSE.MD

* Update LICENSE.md

* Delete LICENSE.MD

* Rename CONTRIBUTORS.MD.md to CONTRIBUTORS.md

* Rename CHANGELOG.MD.md to CHANGELOG.md

* new documents layout

* Update README.md

* Rename mac_install (1).md to mac_install.md

* Rename docker_install.md to docker_install.md

* Rename linux_install.md to linux_install.md

* Update and rename mac_install.md to mac_install.md

* Rename windows_install.md to windows_install.md

* Update docker_install.md

* Update linux_install.md

* Update mac_install.md

* Update windows_install.md

* Update windows_install.md

* Update linux_install.md

* Update tech_stack.md

* Update roadmap.md

* Update project_origin.md

* Update bing_jailbreak_info.md

* Update user_auth_system.md

* Update proxy.md

* Update google_search.md

* Update heroku.md

* Update testing.md

* Update pull_request_template.md

* Update documentation_guidelines.md

* Update contributor_guidelines.md

* Update code_of_conduct.md

* Update README.md

* Update README.md

* Update README.md

* Update roadmap.md

* Update tech_stack.md

* Update feature_request_template.md

* Update bug_report_template.md

* Update custom_issue_template.md

* Update README.md

fix redirect

* Update README.md

dynamic toc

* Update README.md

hide plugins section for now

* Update README.md

removed plugins from TOC

* Update README.md

* Update README.md

* Update documentation_guidelines.md

* Update documentation_guidelines.md

* Update documentation_guidelines.md

directives update

* Update README.md

update shortcut

* Update CHANGELOG.md

* Update roadmap.md

add public trello link

* Update linux_install.md
2023-05-09 13:47:14 -04:00
Danny Avila
177028aafc chore: update docker image version to 0.4.0 (#218)
* chore(docker-compose.yml): update docker-compose file to use local node-api image build instead of docker hub image build

* chore(docker-compose.yml): update docker image version to 0.4.0
2023-05-08 18:57:30 -04:00
Dan Orlando
907f894ba7 fix issue with validation when google account has multiple spaces in name (#211) 2023-05-07 20:31:27 -04:00
Dan Orlando
3b4ed98c1d fix browser refresh redirecting to /chat/new (#210) 2023-05-07 19:16:12 -04:00
Fuegovic
d7b415837b Update README.md (#209)
Fix typos in the google login setup instruction and added the update info in the update section of the readme
2023-05-07 19:11:00 -04:00
Dan Orlando
cc1fcbe949 remove github-passport and update package.lock files (#208) 2023-05-07 16:22:13 -04:00
Dan Orlando
bdcb7acd72 update user system section of readme (#207) 2023-05-07 15:51:18 -04:00
Dan Orlando
960e8c4724 Bump package version and fix spacing in user system section of readme (#206) 2023-05-07 15:27:04 -04:00
Dan Orlando
dac19038a3 feat: Auth and User System (#205)
* server-side JWT auth implementation

* move oauth routes and strategies, fix bugs

* backend modifications for wiring up the frontend login and reg forms

* Add frontend data services for login and registration

* Add login and registration forms

* Implment auth context, functional client side auth

* protect routes with jwt auth

* finish local strategy (using local storage)

* Start setting up google auth

* disable token refresh, remove old auth middleware

* refactor client, add ApiErrorBoundary context

* disable google and facebook strategies

* fix: fix presets not displaying specific to user

* fix: fix issue with browser refresh

* fix: casing issue with User.js (#11)

* delete user.js to be renamed

* fix: fix casing issue with User.js

* comment out api error watcher temporarily

* fix: issue with api error watcher (#12)

* delete user.js to be renamed

* fix: fix casing issue with User.js

* comment out api error watcher temporarily

* feat: add google auth social login

* fix: make google login url dynamic based on dev/prod

* fix: bug where UI is briefly displayed before redirecting to login

* fix: fix cookie expires value for local auth

* Update README.md

* Update LOCAL_INSTALL structure

* Add local testing instructions

* Only load google strategy if client id and secret are provided

* Update .env.example files with new params

* fix issue with not redirecting to register form

* only show google login button if value is set in .env

* cleanup log messages

* Add label to button for google login on login form

* doc: fix client/server url values in .env.example

* feat: add error message details to registration failure

* Restore preventing paste on confirm password

* auto-login user after registering

* feat: forgot password (#24)

* make login/reg pages look like openai's

* add password reset data services

* new form designs similar to openai, add password reset pages

* add api's for password reset

* email utils for password reset

* remove bcrypt salt rounds from process.env

* refactor: restructure api auth code, consolidate routes (#25)

* add api's for password reset

* remove bcrypt salt rounds from process.env

* refactor: consolidate auth routes, use controller pattern

* refactor: code cleanup

* feat: migrate data to first user (#26)

* refactor: use /api for auth routes

* fix: use user id instead of username

* feat: migrate data to first user on register

* fix: fix social login routes after refactor (#27)

* refactor: use /api for auth routes

* fix: use user id instead of username

* feat: migrate data to first user on register

* fix: fix social login routes

* fix: issue with auto-login when logging out then logging in with new browser window (#28)

* refactor: use /api for auth routes

* fix: use user id instead of username

* feat: migrate data to first user on register

* fix: fix social login routes

* fix: fix issue with auto-login in new tab

* doc: Update README and .env.example files with user system information (#29)

* refactor: use /api for auth routes

* fix: use user id instead of username

* feat: migrate data to first user on register

* fix: fix social login routes

* fix: fix issue with auto-login in new tab

* doc: update README and .env.example files

* Fixup: LOCAL_INSTALL.md PS instructions (#200) (#30)

Co-authored-by: alfredo-f <alfredo.fomitchenko@mail.polimi.it>

* feat: send user with completion to protect against abuse (#31)

* Fixup: LOCAL_INSTALL.md PS instructions (#200)

* server-side JWT auth implementation

* move oauth routes and strategies, fix bugs

* backend modifications for wiring up the frontend login and reg forms

* Add frontend data services for login and registration

* Add login and registration forms

* Implment auth context, functional client side auth

* protect routes with jwt auth

* finish local strategy (using local storage)

* Start setting up google auth

* disable token refresh, remove old auth middleware

* refactor client, add ApiErrorBoundary context

* disable google and facebook strategies

* fix: fix presets not displaying specific to user

* fix: fix issue with browser refresh

* fix: casing issue with User.js (#11)

* delete user.js to be renamed

* fix: fix casing issue with User.js

* comment out api error watcher temporarily

* feat: add google auth social login

* fix: make google login url dynamic based on dev/prod

* fix: bug where UI is briefly displayed before redirecting to login

* fix: fix cookie expires value for local auth

* Only load google strategy if client id and secret are provided

* Update .env.example files with new params

* fix issue with not redirecting to register form

* only show google login button if value is set in .env

* cleanup log messages

* Add label to button for google login on login form

* doc: fix client/server url values in .env.example

* feat: add error message details to registration failure

* Restore preventing paste on confirm password

* auto-login user after registering

* feat: forgot password (#24)

* make login/reg pages look like openai's

* add password reset data services

* new form designs similar to openai, add password reset pages

* add api's for password reset

* email utils for password reset

* remove bcrypt salt rounds from process.env

* refactor: restructure api auth code, consolidate routes (#25)

* add api's for password reset

* remove bcrypt salt rounds from process.env

* refactor: consolidate auth routes, use controller pattern

* refactor: code cleanup

* feat: migrate data to first user (#26)

* refactor: use /api for auth routes

* fix: use user id instead of username

* feat: migrate data to first user on register

* fix: fix social login routes after refactor (#27)

* refactor: use /api for auth routes

* fix: use user id instead of username

* feat: migrate data to first user on register

* fix: fix social login routes

* fix: issue with auto-login when logging out then logging in with new browser window (#28)

* refactor: use /api for auth routes

* fix: use user id instead of username

* feat: migrate data to first user on register

* fix: fix social login routes

* fix: fix issue with auto-login in new tab

* doc: Update README and .env.example files with user system information (#29)

* refactor: use /api for auth routes

* fix: use user id instead of username

* feat: migrate data to first user on register

* fix: fix social login routes

* fix: fix issue with auto-login in new tab

* doc: update README and .env.example files

* Send user id to openai to protect against abuse

* add meilisearch to gitignore

* Remove webpack

---------

Co-authored-by: alfredo-f <alfredo.fomitchenko@mail.polimi.it>

---------

Co-authored-by: Danny Avila <110412045+danny-avila@users.noreply.github.com>
Co-authored-by: Alfredo Fomitchenko <alfredo.fomitchenko@mail.polimi.it>
2023-05-07 10:04:51 -07:00
alfredo-f
65543eb084 Fixup: LOCAL_INSTALL.md PS instructions (#200) 2023-05-03 07:16:38 -04:00
Danny Avila
cc18938235 Merge pull request #196 from alfredo-f/alfredo/playwright
Add instructions for local testing.
2023-04-30 06:54:45 -04:00
Alfredo Fomitchenko
1049b403c3 Add local testing instructions 2023-04-30 08:53:30 +02:00
Danny Avila
fb3fc55e9f Merge pull request #195 from alfredo-f/alfredo/local_install
Update LOCAL_INSTALL structure
2023-04-29 16:34:22 -04:00
Alfredo Fomitchenko
d07e5f5241 Update LOCAL_INSTALL structure 2023-04-29 21:13:33 +02:00
Danny Avila
f7114c16c2 Update README.md 2023-04-28 08:15:35 -04:00
Danny Avila
4699ad21c7 Merge pull request #188 from fuegovic/main
Added a link for the "automated installer"
2023-04-26 21:34:39 -04:00
Fuegovic
766bd0c587 Update README.md
layout fix for previous changes
2023-04-26 16:26:25 -04:00
Fuegovic
9116e98928 Update README.md
added information under the fuegovic's automated installer link
2023-04-26 16:24:37 -04:00
Fuegovic
8e8ccb9c8b Update LOCAL_INSTALL.md
Layout Updates
2023-04-26 16:21:36 -04:00
Fuegovic
5fd238af64 Update README.md
added a link for fuegovic's automated installer
2023-04-26 16:17:46 -04:00
Danny Avila
03f4e89f1c Merge pull request #187 from fuegovic/main
Update .env.example
2023-04-25 14:29:30 -04:00
Fuegovic
75cef1ebb1 Update .env.example
Added clarification about the `user_provided` token use
2023-04-25 13:46:47 -04:00
Fuegovic
857481c263 Update .env.example
added back the lines : # Set to "user_provided" to allow user provided token.
2023-04-25 13:09:24 -04:00
Danny Avila
8f462e074c Merge pull request #186 from zhangsean/patch-1
Add container name
2023-04-25 10:00:09 -04:00
Fuegovic
3eddc9712f Update .env.example
I continued the work on the standardization of the layout. I also eliminated duplicate key=value pairs to simplify the configuration and reduce the likelihood of errors. I also updated some of the commented instructions and notes throughout the file to keep the instructions clear while making it easier to prevent errors when using a script to parse the key=value pairs.
2023-04-25 04:26:38 -04:00
Sean Zhang
52f99151ec Fix container name 2023-04-25 10:14:13 +08:00
Sean Zhang
d839ea324a Add container name 2023-04-25 10:04:38 +08:00
Danny Avila
e02e6152ed Merge pull request #183 from danny-avila/fix-edit-wrap
add whitespace-pre-wrap to the message editor to preserve line breaks
2023-04-21 23:13:42 -04:00
Daniel Avila
2b7c1507ef style(Message.jsx): add whitespace-pre-wrap to the message editor to preserve line breaks 2023-04-21 23:12:45 -04:00
Danny Avila
f5ff91cfbd build(Dockerfile-app): add Dockerfile for building the app image
feat(docker-compose.yml): use pre-built image for api service instead of building from local file
2023-04-21 14:38:23 -04:00
Daniel Avila
901375bfa0 fix(chatgpt-browser.js): update public reverse proxy URL 2023-04-20 13:13:47 -04:00
Danny Avila
15a734b6c8 Update README.md 2023-04-16 23:12:59 -07:00
Danny Avila
4f119296f4 Update README.md 2023-04-14 18:11:20 -07:00
Daniel Avila
b88828e29a feat(api): update @waylaidwanderer/chatgpt-api dependency to version 1.35.0 2023-04-12 05:47:10 -07:00
Danny Avila
3447137515 Merge pull request #179 from danny-avila/fix-unknown-conversationId
fix: fix infinite query failure when conversationId is not found
2023-04-11 19:52:42 -06:00
Daniel Avila
6aa540c4af fix(api): correct typo in environment variable name from "user_provide" to "user_provided" in bingai.js and chatgpt-browser.js clients and endpoints.js router 2023-04-11 21:50:53 -04:00
Danny Avila
163388b8a9 Merge pull request #177 from danny-avila/user-providered-key
User providered key and unfinished messages.
2023-04-11 19:46:28 -06:00
Daniel Avila
e68c163ef6 feat(Input/NewConversationMenu): save last selected model to localStorage
fix(getDefaultConversation.js): use last selected model from localStorage if available
2023-04-11 21:33:14 -04:00
Daniel Avila
dcf2ee480b refactor(tokenizer.js): remove console.log statement from tokenizer.js 2023-04-11 10:29:55 -04:00
Daniel Avila
5c5871afd8 style(endpoints.js): fix indentation and add semicolons
fix(tokenizer.js): add try-catch block and error handling
style(SetTokenDialog/index.jsx): fix typo in sentence
refactor(data-service.ts): change argument format to match server API
2023-04-11 10:28:11 -04:00
Daniel Avila
e0d5e75e73 fix: only give initialResponse unfinished true value when not a cancellable endpoint 2023-04-10 18:27:06 -04:00
Daniel D Orlando
fb7542c865 fix: fix infinite query failure when conversationId is not found 2023-04-10 14:55:39 -07:00
Daniel Avila
0bd240939a refactor(Message.jsx): remove cancelled message bubble and improve wording of unfinished message 2023-04-10 17:15:28 -04:00
Daniel Avila
31893ec9f6 chore: bump version to 0.3.3 in package.json files 2023-04-10 17:06:49 -04:00
Daniel Avila
78ae220f7e fix(Input): fix isNotAppendable condition to include isSubmitting variable
fix(Input): prevent submitting message when isSubmitting is true and Enter key is pressed
2023-04-10 17:03:11 -04:00
Jinrui
478814ff1b fix nginx container copying from wrong folder 2023-04-11 03:51:56 +08:00
Wentao Lyu
fd0152e39f fix: typo 2023-04-11 03:50:37 +08:00
Wentao Lyu
f5f327e79e mark initial response as unfinished. 2023-04-11 03:42:05 +08:00
Wentao Lyu
9871127114 cleanup 2023-04-11 03:29:54 +08:00
Wentao Lyu
a5a0eab7f7 merge from dannya
feat: support unfinished messages.
2023-04-11 03:26:38 +08:00
Wentao Lyu
bbf2f8a6ca feat: support user-provided token to bingAI and chatgptBrowser 2023-04-10 14:51:38 +08:00
Daniel Avila
a953fc9f2b wip: feat: abort messages and continue conversation
fix(addToCache.js): remove unused variables and parameters
feat(addToCache.js): add message to cache with id, parentMessageId, role, and text
fix(askOpenAI.js): remove parentMessageId parameter from addToCache call
feat(MessageHandler.jsx): add latestMessage to store on cancel of submission, and generate messageId and parentMessageId for latestMessage
2023-04-09 22:21:27 -04:00
Daniel Avila
a81bd27b39 feat(api): add support for saving messages to database
fix(api): change arrowParens prettier option to always
fix(api): update addToCache to include endpointOption and latestMessage
fix(api): update askOpenAI to include endpointOption in abortControllers
fix(client): remove abortKey state and add currentParent state to MessageHandler
2023-04-09 11:17:08 -04:00
Daniel Avila
6246ffff1e wip: refactor: new abort message handling 2023-04-09 09:23:03 -04:00
Daniel Avila
b59588c6ee refactor(messageHandler): sends all necessary data to cache/save unfinished response 2023-04-09 09:22:14 -04:00
Daniel Avila
828e438d53 feat(api-endpoints.ts): add abortRequest endpoint
feat(data-service.ts): add abortRequestWithMessage function
feat(react-query-service.ts): add useAbortRequestWithMessage hook
2023-04-09 09:21:04 -04:00
Daniel Avila
f946f90ef6 Merge branch 'main' into fix-sse 2023-04-09 07:54:06 -04:00
Daniel Avila
956d919751 fix(Messages/index.jsx): import lodash throttle function efficiently 2023-04-09 07:52:42 -04:00
Daniel Avila
4f1fc3f020 chore(package-lock.json): remove duplicate media-typer dependency and update its version to 0.3.0 in type-is and express dependencies. 2023-04-09 07:47:08 -04:00
Daniel Avila
c1b6f96bca Merge branch 'main' into fix-sse 2023-04-09 07:41:55 -04:00
Danny Avila
ae1333db37 Update README.md 2023-04-09 07:40:03 -04:00
Danny Avila
19c0f00cf2 Update README.md 2023-04-09 07:39:41 -04:00
Danny Avila
3c8ac933ee Merge pull request #176 from danny-avila/minor-fixes
Minor fixes
2023-04-09 07:38:28 -04:00
Daniel Avila
50ec165f71 chore: update package versions to 0.3.2 2023-04-09 07:37:57 -04:00
Daniel Avila
942a85f531 fix: vite build issue: set memory limit for Node.js to 2048 MB 2023-04-09 07:35:10 -04:00
Daniel Avila
83d0443c8a fix: page no longer refreshes on stop generating button 2023-04-09 07:32:07 -04:00
Daniel Avila
88aea81288 WIP: fix: fix abort messages and continue conversation on abort
feat(askOpenAI.js): add abort endpoint to cancel requests
feat(MessageHandler): add abort functionality to cancel requests
feat(submission.js): add lastResponse and source atoms to store
feat(handleSubmit.js): add stopGenerating function to cancel requests
2023-04-08 23:19:29 -04:00
Danny Avila
5fbefa15ce Merge pull request #171 from git-bruh/main
fix: replace various anchor tags with buttons to prevent text selection on repeated clicks
2023-04-08 08:38:41 -04:00
Daniel Avila
0f62be812a refactor(AdjustToneButton.jsx): replace svg icon with lucide-react Settings2 icon 2023-04-08 08:37:44 -04:00
Danny Avila
92c1bb6511 Merge pull request #174 from HyunggyuJang/fix/reset-after-title-generation
Fix #119
2023-04-08 08:32:13 -04:00
Danny Avila
cb56b74b0e Merge pull request #160 from HyunggyuJang/sydney/tone-adjustable
Make sydney tone adjustable during conversation
2023-04-08 08:30:05 -04:00
Daniel Avila
94078ce5d4 chore: remove GA until more tests are written 2023-04-08 08:21:34 -04:00
Hyunggyu Jang
e5e4ee2987 Fix error 2023-04-08 13:32:15 +09:00
Hyunggyu Jang
100be3b42f Make sydney tone adjustable 2023-04-08 13:29:08 +09:00
Danny Avila
0a80f836f0 Merge pull request #165 from danny-avila/dano/react-query-typescript
Refactor: Create data-provider for api services with React Query and TypeScript
2023-04-07 22:14:44 -04:00
Daniel Avila
8b952be268 refactor(NewConversationMenu): remove unused imports and commented code
feat(NewConversationMenu): add support for deleting presets and creating new presets using data-provider
2023-04-07 22:11:28 -04:00
Daniel Avila
285351bb53 Merge branch 'main' into dano/react-query-typescript 2023-04-07 22:11:15 -04:00
Danny Avila
625f63b072 Merge pull request #173 from danny-avila/feat-new-titleconvo
Feat new titleconvo
2023-04-07 20:50:14 -04:00
Daniel D Orlando
bf4258c0a5 Add react query devtools 2023-04-07 16:13:00 -07:00
Daniel D Orlando
9a0e3804fa fix nav pagination 2023-04-07 10:16:53 -07:00
Wentao Lyu
90946011f7 fix: update titleConvo to use same title protocal as node-chatgpt-api 2023-04-08 00:36:58 +08:00
Wentao Lyu
06b90f6a77 feat: add host params to bingAI.
[but seems not work in China]
2023-04-08 00:14:44 +08:00
Wentao Lyu
96b004a696 feat: add animation to New Topic. 2023-04-08 00:14:15 +08:00
Wentao Lyu
9623fe2e9f clean code with newConversationId in askbingai
Revert "Merge pull request #167 from danny-avila/fix-sydney"

This reverts commit 15999bda95, reversing
changes made to e1c6517b8f.
2023-04-07 23:13:13 +08:00
Danny Avila
d79d297441 Merge pull request #172 from danny-avila/dano/fix-vite-memory-allocation-issue
Fix: fix javascript heap out of memory error from vite
2023-04-07 10:44:48 -04:00
Daniel D Orlando
fd5fba45e6 remove unnecessary code 2023-04-07 07:36:28 -07:00
Daniel D Orlando
6e42d4fa3d Fix: fix javascript heap out of memory error from vite 2023-04-07 07:27:05 -07:00
Danny Avila
5ea8f75f70 Merge branch 'main' into dano/react-query-typescript 2023-04-07 10:19:13 -04:00
Danny Avila
7ec90a3585 Merge pull request #168 from danny-avila/feat-export-convo
fix: remove use-screenshot
2023-04-07 10:15:43 -04:00
git-bruh
fc91ed49bc fix: replace various anchor tags with buttons to prevent text selection on repeated clicks 2023-04-07 19:25:30 +05:30
Danny Avila
6ce1b9d850 Update README.md 2023-04-07 09:42:36 -04:00
Daniel D Orlando
c983670b9e Fix bug - when clicking on title in search results, was not switching to conversation 2023-04-07 06:02:28 -07:00
Daniel D Orlando
e0f9e92bfc fix bad setState warning in console 2023-04-07 05:20:14 -07:00
Wentao Lyu
ca720efde8 fix: remove use-screenshot
feat: displat file type
2023-04-07 13:21:20 +08:00
Danny Avila
34c3663308 Update README.md 2023-04-07 00:06:05 -04:00
Danny Avila
15999bda95 Merge pull request #167 from danny-avila/fix-sydney
fix: undo use of newConversationId which broke sydney
2023-04-06 20:26:29 -04:00
Daniel Avila
6a77401978 fix: undo commit 3b94a98 which broke sydney 2023-04-06 20:25:28 -04:00
Danny Avila
644ff160fc Merge branch 'main' into dano/react-query-typescript 2023-04-06 19:47:36 -04:00
Danny Avila
e1c6517b8f Merge pull request #164 from danny-avila/workflow
ci(playwright.yml): add Playwright tests workflow
2023-04-06 19:43:36 -04:00
Daniel Avila
8a243e12fb chore(landing.spec.js): comment out title expectation and update landing title text 2023-04-06 19:39:19 -04:00
Daniel Avila
3f42db4956 chore(playwright.yml): move 'Start API server' job to after 'Install Playwright Browsers' job
fix(landing.spec.js): change page.goto URL to 'http://localhost:3080/'
2023-04-06 19:33:33 -04:00
Daniel Avila
323e951d7f ci(playwright.yml): add environment variables for secrets BINGAI_TOKEN, CHATGPT_TOKEN, MONGO_URI, and OPENAI_KEY 2023-04-06 19:26:25 -04:00
Danny Avila
11e4928582 Merge pull request #166 from danny-avila/fix-format
fix: formatting for user messages
2023-04-06 18:59:40 -04:00
Daniel Avila
24cb6d4013 fix: formatting for user messages 2023-04-06 18:58:56 -04:00
Daniel Avila
19183678a3 build(playwright.yml): add caching for API and Client dependencies
Add caching for API and Client dependencies to speed up the build process.
2023-04-06 18:52:13 -04:00
Daniel D Orlando
dccd766d91 remove unused import 2023-04-06 15:50:15 -07:00
Daniel D Orlando
d24abf6a2a Cleanup App.jsx 2023-04-06 15:42:39 -07:00
Daniel Avila
6b1b0f967d build(playwright.yml): add API and Client dependencies installation and server start before running tests
feat(playwright.yml): add global dependencies installation before running tests
2023-04-06 18:38:12 -04:00
Daniel Avila
a56c8696d3 fix(playwright.config.js): correct baseURL typo from 'http:/localhost:3080' to 'http://localhost:3080' 2023-04-06 18:31:02 -04:00
Daniel D Orlando
7b7ba96786 Move createPayload and sse to data-provider, create TSubmission type 2023-04-06 14:56:33 -07:00
Daniel D Orlando
0fb9820110 change fetchById to call getConversationById 2023-04-06 14:56:33 -07:00
Daniel D Orlando
c271f044c7 remove swr 2023-04-06 14:56:33 -07:00
Daniel D Orlando
ec2ddc168b delete fetchers.js 2023-04-06 14:56:33 -07:00
Daniel D Orlando
0d5b51ec8c use create preset mutation for preset import 2023-04-06 14:56:33 -07:00
Daniel D Orlando
06a7ed31ac remove unused endpoints 2023-04-06 14:56:33 -07:00
Daniel D Orlando
4eff1c03dd package.lock 2023-04-06 14:56:33 -07:00
Daniel D Orlando
83df28f45d add RQ tokenizer 2023-04-06 14:56:33 -07:00
Daniel D Orlando
48e33fe1e9 Add support for deleting individual presets 2023-04-06 14:56:33 -07:00
Daniel D Orlando
fbeff7a461 Code cleanup 2023-04-06 14:56:33 -07:00
Daniel D Orlando
61cb2858bb refactor and optimize search, add RQ for search 2023-04-06 14:56:33 -07:00
Daniel D Orlando
3d0bfaef51 Add presets and endpoints data services 2023-04-06 14:56:33 -07:00
Daniel D Orlando
f2d18c81fc add deletePresetMutation to NewConverationMenu 2023-04-06 14:56:33 -07:00
Daniel D Orlando
68041d68ae fix types 2023-04-06 14:56:33 -07:00
Daniel D Orlando
93b685a1a2 change endpoints.ts to api-endpoints.ts 2023-04-06 14:56:33 -07:00
Daniel D Orlando
9e708225aa Add preset mutation 2023-04-06 14:56:33 -07:00
Daniel D Orlando
1cb8ef9803 feat: convert Chat.jsx to RQ 2023-04-06 14:56:00 -07:00
Daniel D Orlando
573112de7b fix: fix conversations in nav (put refreshConvoHint back) 2023-04-06 14:56:00 -07:00
Daniel D Orlando
dd0a91a9f6 add RQ to clear all conversations 2023-04-06 14:56:00 -07:00
Daniel D Orlando
94e0636b32 add delete conversation mutation, fix withAuthentication on post requests 2023-04-06 14:56:00 -07:00
Daniel D Orlando
bd53b878d4 update react-query-service 2023-04-06 14:56:00 -07:00
Daniel D Orlando
c6d3bcd457 feat: Add RQ to Conversation component, create temp Chat component with RQ for compare and debugging 2023-04-06 14:56:00 -07:00
Daniel D Orlando
39f53e6ddf add QueryClientProvider to main 2023-04-06 14:56:00 -07:00
Daniel D Orlando
10941bf623 add DS_Store to gitignore 2023-04-06 14:56:00 -07:00
Daniel D Orlando
8c392ac05e build: add react query 2023-04-06 14:56:00 -07:00
Daniel D Orlando
9dae1ade60 turn off no-debugger in eslintrc for debugginer purposes 2023-04-06 14:55:26 -07:00
Daniel D Orlando
ccc2f392e2 feat: add conversation query to nav 2023-04-06 14:55:26 -07:00
Daniel D Orlando
2048e34311 feat: add new data services to App.jsx 2023-04-06 14:55:26 -07:00
Daniel D Orlando
2589754171 feat: add data-provider 2023-04-06 14:54:37 -07:00
Daniel Avila
4510f04073 feat(playwright.yml): add GitHub Actions workflow for running Playwright tests on push and pull request events on main and master branches 2023-04-06 17:34:47 -04:00
Daniel Avila
e98ce09d6b ci(playwright.yml): add Playwright tests workflow
fix(Landing.jsx): add id attribute to landing page title
test(landing.spec.js): update landing page title and h1 text content assertions
2023-04-06 17:33:02 -04:00
Danny Avila
21920dd864 Merge pull request #155 from danny-avila/feat-export-convo
Feature support export conversation
2023-04-06 17:11:59 -04:00
Daniel Avila
7d45d229af refactor(PresetItem.jsx): swap order of the edit and delete preset buttons 2023-04-06 17:07:44 -04:00
Daniel Avila
5dad9da918 chore(docker-compose.yml): instructions for CHATGPT_REVERSE_PROXY .env variable 2023-04-06 17:06:42 -04:00
Daniel Avila
e0b0b68346 feat(Conversation.jsx): set document title when conversation is switched 2023-04-06 16:52:05 -04:00
Danny Avila
31cef16cc3 fix: fileName formatting fixes 2023-04-06 16:18:36 -04:00
Danny Avila
4245b43140 fix: used forked repo of use-react-screenshot for dep updates 2023-04-06 16:00:05 -04:00
Wentao Lyu
5664a0c2a5 fix: remove blank in screenshot 2023-04-07 02:00:51 +08:00
Danny Avila
dde6de6bd5 Merge pull request #163 from danny-avila/fix-markdown
fix: markdown formatting errors
2023-04-06 13:55:04 -04:00
Danny Avila
e077f2b73d refactor(askChatGPTBrowser.js): remove unused titleConvo import
style(Message.jsx, style.css): adjust margins and paddings to improve readability
2023-04-06 13:53:19 -04:00
Wentao Lyu
6e8a0a2f94 fix: didnt use preset from a exist convo 2023-04-07 01:49:28 +08:00
Wentao Lyu
96914387a6 feat: export to screenshot 2023-04-07 00:05:07 +08:00
Wentao Lyu
6f0b559927 feat: export conversation: csv, json, txt, markdown 2023-04-07 00:05:07 +08:00
Wentao Lyu
3b94a98719 fix: use new conversation Id 2023-04-07 00:05:07 +08:00
Daniel Avila
017447b064 chore(api): update chatgpt-api dependency to version 1.34.0
feat(api): use gen_title response for askChatGPTBrowser.js (official title)
2023-04-05 21:34:39 -04:00
Danny Avila
385eb2f398 Update README.md 2023-04-05 17:09:04 -04:00
Danny Avila
963939fe76 Update README.md 2023-04-05 17:02:00 -04:00
Danny Avila
cf3902567e Update README.md 2023-04-05 16:58:54 -04:00
Danny Avila
b4d451d7ae Merge pull request #142 from danny-avila/feat-endpoint-style-structure
Endpoint style structure and more customize of all model.
2023-04-05 15:07:27 -04:00
Danny Avila
97e39b8203 Merge branch 'main' into feat-endpoint-style-structure 2023-04-05 14:06:37 -04:00
Danny Avila
60f51dfeeb fix: remove messageHeader click when endpoint is browser 2023-04-05 14:05:54 -04:00
Wentao Lyu
e7a6589958 fix: lost model of browser 2023-04-06 01:55:28 +08:00
Danny Avila
d4cd9411c0 feat: add delete for presets in menu
[200~refactor(presets.js): remove unused getPreset function
refactor(presets.js): use arrow function syntax for map callback
refactor(presets.js): add console.log for debugging purposes
refactor(presets.js): simplify map callback syntax
refactor(presets.js): remove commented out code
refactor(FileUpload.jsx): remove commented out code
refactor(NewConversationMenu.jsx): rename data parameter to res for
clarity
refactor(NewConversationMenu.jsx): rename clearPresetsTrigger to
deletePresetsTrigger for clarity
refactor(NewConversationMenu.jsx): add onDeletePreset prop to
PresetItems component
refactor(PresetItem.jsx): add TrashIcon component and onDeletePreset
prop
refactor(PresetItems.jsx): add onDeletePreset prop to PresetItem
component
2023-04-05 13:21:29 -04:00
Danny Avila
e1c731299c fix(ui): change SelectDropdown to SelectDropDown in multiple files 2023-04-05 12:00:52 -04:00
Danny Avila
214067cfcb feat(client): add doubleClickHandler prop to Slider component to reset to default
feat(client): add doubleClickHandler to temperature, topP, freqP, and presP sliders in OpenAI Settings component
2023-04-05 11:28:20 -04:00
Danny Avila
7d3df376ef style(Settings.jsx): add openDelay prop for faster HoverCard views 2023-04-05 10:29:06 -04:00
Wentao Lyu
474ced1dbe fix: typo in askBingAI.js 2023-04-05 22:13:38 +08:00
Danny Avila
55e949578f style(MessageHeader.jsx): add hover opacity to the background color of the message header 2023-04-05 10:03:52 -04:00
Danny Avila
b730d631af feat(client): add rc-input-number package to dependencies
feat(client): add classnames package to dependencies
2023-04-05 09:41:46 -04:00
Wentao Lyu
03cade8bd5 fix: input style
fix: use ChatGptBrowser
2023-04-05 21:21:31 +08:00
Wentao Lyu
22b9524ad3 feat: update env example.
feat: support OPENAI_REVERSE_PROXY
feat: support set availModels in env file

fix: chatgpt Browser send logic refactor.

fix: title wrong usage of responseMessage
BREAKING: some env paramaters has been changed!
2023-04-05 21:21:02 +08:00
Wentao Lyu
a5202f84cc fix: force reset to default if 0 or false 2023-04-05 17:25:35 +08:00
Wentao Lyu
4b373ebc0e feat: range number input in options. 2023-04-05 17:14:05 +08:00
Wentao Lyu
0bc2db08c7 style: options in mobile 2023-04-05 16:27:11 +08:00
Wentao Lyu
a641a0e373 fix: dont clear the input message if conversationId changed. 2023-04-05 16:16:26 +08:00
Wentao Lyu
6abe34ee3b fix: validate the import preset 2023-04-05 16:16:11 +08:00
Wentao Lyu
8c2d577e60 feat: remove customGpts 2023-04-05 16:15:46 +08:00
Daniel Avila
5aa6b516ed feat: Import Presets from NewConversationMenu
feat(FileUpload.jsx): add support for importing JSON files and fetching presets after import
feat(NewConversationMenu.jsx): add FileUpload component to select and import JSON files
feat(fetchers.js): add handleFileSelected function to handle importing JSON files and uploading presets to the server
2023-04-04 23:18:58 -04:00
Daniel Avila
0846aa0436 docs(bingai.js): remove commented example response and add reference to demo file 2023-04-04 21:00:04 -04:00
Daniel Avila
3ab9b7f736 refactor(askOpenAI.js): rename gptResponse to response, fixes titling error 2023-04-04 18:17:23 -04:00
Wentao Lyu
56e02f4968 change Switch to simple mode to close button 2023-04-05 03:58:22 +08:00
Wentao Lyu
46fba87f9a feat: add confirm of clear all presets. 2023-04-05 03:50:06 +08:00
Wentao Lyu
ee10e0e43e feat: use default preset to create new conversation. 2023-04-05 03:49:54 +08:00
Wentao Lyu
579b53de29 feat: style match 2023-04-05 03:07:46 +08:00
Wentao Lyu
9f1ded7f75 feat: add jailbreak option for bingai
fix some bugs.
2023-04-05 02:46:22 +08:00
Wentao Lyu
efb440128a code clean. 2023-04-05 02:30:25 +08:00
Wentao Lyu
010d900c90 fix: use universal setToneStyle 2023-04-05 02:30:14 +08:00
Wentao Lyu
0c4b754fba fix: to set default value when change endpoint of preset 2023-04-05 02:29:43 +08:00
Wentao Lyu
d864da6a21 fix: rewrite ask openAI and ask BingAI. now all code cleaned. 2023-04-05 02:29:11 +08:00
Danny Avila
0ec68bf5a8 Update defaultSystemMessage.md 2023-04-04 13:35:34 -04:00
Danny Avila
82b4401e49 Update defaultSystemMessage.md 2023-04-04 13:35:10 -04:00
Danny Avila
99d238b0de Update defaultSystemMessage.md 2023-04-04 13:30:24 -04:00
Danny Avila
617aa6f499 Update defaultSystemMessage.md 2023-04-04 13:30:04 -04:00
Danny Avila
f922a1d102 Merge branch 'feat-endpoint-style-structure' of https://github.com/danny-avila/chatgpt-clone into feat-endpoint-style-structure 2023-04-04 12:53:59 -04:00
Danny Avila
bb75b6df3b feat(bingai.js): add context and systemMessage parameters to askBing function
feat(conversationPreset.js): add context and systemMessage fields to conversation preset schema
feat(askBingAI.js): pass context and systemMessage parameters to ask function
feat(Settings.jsx): add toneStyle prop to BingAISettings component
feat(BingAIOptions/index.jsx): add useEffect to check if advanced mode is needed
feat(cleanupPreset.js): add context and systemMessage fields to cleaned up preset object
feat(getDefaultConversation.js): add context and systemMessage fields to default conversation object
feat(handleSubmit.js): add context and systemMessage fields to message object
2023-04-04 12:53:41 -04:00
Danny Avila
4b04959290 Update defaultSystemMessage.md 2023-04-04 11:22:23 -04:00
Danny Avila
28c5b21217 Update defaultSystemMessage.md 2023-04-04 11:21:30 -04:00
Daniel D Orlando
086267ee01 Add esbuild to package.json 2023-04-04 07:32:28 -07:00
Danny Avila
3484ff687d Merge branch 'feat-endpoint-style-structure' of https://github.com/danny-avila/chatgpt-clone into feat-endpoint-style-structure 2023-04-04 10:08:12 -04:00
Danny Avila
6e233accf5 style(tokenizer.js): comment out console.log statement
fix(Settings.jsx): handle null or empty context in useEffect to avoid errors
2023-04-04 10:07:48 -04:00
Danny Avila
1a196580b2 feat: count tokens for context
feat(BingAI): convert toneStyle to lowercase before setting it in state
feat(BingAI): pass setToneStyle function to Settings component
refactor(BingAI): remove unused import and change setOption to setToneStyle in Settings component
refactor(fetchers.js): add axiosPost function for debounced axios post requests
2023-04-04 10:04:21 -04:00
Wentao Lyu
e1d52b858b fix: cannot open edit preset the use preset. 2023-04-04 22:01:01 +08:00
Wentao Lyu
cef98070e9 style: show scrollbar over content. 2023-04-04 13:27:09 +08:00
Wentao Lyu
7353829719 refactor: code clean up 2023-04-04 13:11:59 +08:00
Daniel Avila
89e38d67f4 feat(bing-settings): Work in Progress, will finish tomorrow
feat(api): add @dqbd/tiktoken package as a dependency
feat(api): add /api/tokenizer endpoint to tokenize text using @dqbd/tiktoken
feat(client): add toneStyle dropdown to BingAI Settings component
feat(client): add token count to BingAI Settings component

style(ui): add z-index to Dropdown and EndpointOptionsPopover components

Add z-index to Dropdown and EndpointOptionsPopover components to ensure they are displayed above other components.
2023-04-03 21:18:19 -04:00
Danny Avila
825be5aa4d Update defaultSystemMessage.md 2023-04-03 20:12:56 -04:00
Danny Avila
d8deb89e3a Update defaultSystemMessage.md 2023-04-03 20:07:24 -04:00
Danny Avila
da69e4176a Create defaultSystemMessage.md 2023-04-03 20:06:55 -04:00
Danny Avila
85372118ed Delete defaultSystemMessage 2023-04-03 20:06:36 -04:00
Danny Avila
4fe48fe6bb Create defaultSystemMessage 2023-04-03 20:06:08 -04:00
Daniel Avila
03f63975cc Merge branch 'main' into feat-endpoint-style-structure 2023-04-03 17:30:59 -04:00
Danny Avila
28f1e851f9 chore(Settings.jsx): comment out unused import of InputNumber component 2023-04-03 16:29:02 -04:00
Danny Avila
58d162d8b4 reset package-lock files 2023-04-03 14:39:27 -04:00
Danny Avila
4961cc9250 reset package-lock on root dir 2023-04-03 14:24:32 -04:00
Wentao Lyu
8720f39def feat: preserve the title of preset. 2023-04-04 01:39:40 +08:00
Wentao Lyu
089d99e679 feat: open option dialog when click messageHeader 2023-04-04 01:23:37 +08:00
Wentao Lyu
d2579b44d1 feat: support edit preset,
feat: support view current conversation options.
feat: save current conversation as a preset.
feat: preset delete all.
2023-04-04 01:12:14 +08:00
Wentao Lyu
dae0c2d5e3 server fix: code refactor 2023-04-04 01:10:50 +08:00
Danny Avila
3d246df6c9 Merge pull request #149 from adamrb/main
Support local config for reverse proxy
2023-04-03 13:07:45 -04:00
Wentao Lyu
ea88e9b981 fix: bingAI/settings: wrong usage of checkbox 2023-04-04 01:04:33 +08:00
Adam Boeglin
584772b3f2 Support local config for reverse proxy 2023-04-03 09:29:52 -07:00
Wentao Lyu
e7c9ae5a7d fix: regenerate should work for error message. 2023-04-03 21:32:21 +08:00
Wentao Lyu
4a95b30a0d fix: proxy auth in vite.config.js 2023-04-03 21:31:59 +08:00
Wentao Lyu
15ada95249 feat: export preset. 2023-04-03 12:54:15 +08:00
Daniel Avila
ced65f8c98 feat(client): add @radix-ui/react-checkbox dependency and create BingAI Settings component
feat(BingAIOptions): add advanced mode to BingAIOptions component
feat(Checkbox): add Checkbox component to ui components
2023-04-02 18:45:41 -04:00
Daniel Avila
871bc4c78b refactor(getIcon.jsx): if model is GPT-4, change background color to black. 2023-04-02 15:23:28 -04:00
Daniel Avila
24a82c8018 Merge branch 'main' into feat-endpoint-style-structure 2023-04-02 15:10:21 -04:00
Danny Avila
cd7e3f3774 Merge pull request #148 from danny-avila/update-proxy
fix(chatgpt-browser.js): update reverseProxyUrl to use new domain nam…
2023-04-02 14:54:47 -04:00
Daniel Avila
ab7cf8c881 fix(chatgpt-browser.js): update reverseProxyUrl to use new domain name 'https://bypass.churchless.tech' instead of 'https://bypass.duti.tech' 2023-04-02 14:54:02 -04:00
Daniel Avila
aa4fd57459 feat(chatgpt-browser): add support for multiple GPT models
This commit adds support for multiple GPT models in the chatGPT-browser
client. The available models are now stored in a Map object, which maps
the model label to its corresponding model.

The commit adds a new component, ChatGPTOptions, to the client
UI to allow the user to select the GPT model to use in the chat. The
component is only displayed when the chatGPT-browser endpoint is
selected.
2023-04-02 14:34:12 -04:00
Danny Avila
8551995a51 Merge pull request #138 from danny-avila/danorlando/setup-e2e-with-playwright
test: Playwright setup
2023-04-02 14:28:23 -04:00
Daniel D Orlando
85f3b488ce test: move playwright action out of workflows 2023-04-02 11:11:53 -07:00
Daniel D Orlando
12b7e9a6bb reset back to npm ci 2023-04-02 11:07:48 -07:00
Daniel D Orlando
d5e062eeed try using npm install instead of npm ci in github action 2023-04-02 11:07:48 -07:00
Daniel D Orlando
f5e120c330 try explicitly setting registry before npm ci step 2023-04-02 11:07:48 -07:00
Daniel D Orlando
afaa0253b8 undo change adding .npmrc to try to resolve the package resolution issue on npm ci step of github action 2023-04-02 11:07:48 -07:00
Daniel D Orlando
3ec2942365 try manually removing @playwright from the resolution path to make github action work 2023-04-02 11:07:48 -07:00
Daniel D Orlando
6d7f0448ff try adding .npmrc to resolve problem getting playwright-core from the registry 2023-04-02 11:07:48 -07:00
Daniel D Orlando
70427a628f generate new package-lock.json 2023-04-02 11:07:48 -07:00
Daniel D Orlando
b19ef425b7 try removing package-lock.json and forcing it to generate on the npm ci step 2023-04-02 11:07:48 -07:00
Daniel D Orlando
b7c911534c use node 18 2023-04-02 11:07:48 -07:00
Daniel D Orlando
fe6b1fc12a generate new package-lock.json 2023-04-02 11:07:48 -07:00
Daniel D Orlando
bd94c4233d Playwright setup 2023-04-02 11:07:48 -07:00
Daniel Avila
eef2303c8e Merge branch 'main' into feat-endpoint-style-structure 2023-04-02 12:10:14 -04:00
Danny Avila
6f30b6ec9d Merge pull request #147 from danny-avila/lift-api
chore(api): update "@waylaidwanderer/chatgpt-api" dependency to versi…
2023-04-02 12:08:33 -04:00
Daniel Avila
709dbbbc4b chore(api): update "@waylaidwanderer/chatgpt-api" dependency to version 1.33.2 2023-04-02 12:07:59 -04:00
Daniel Avila
77b46cc1a7 refactor(ModelDropDown.jsx): remove commented out code and simplify model display 2023-04-02 11:37:10 -04:00
Danny Avila
f069d6b621 Update README.md 2023-04-02 08:27:38 -04:00
Daniel Avila
f187da3afa reset package-lock 2023-04-02 00:03:54 -04:00
Daniel Avila
0564e3ed93 Merge branch 'main' into feat-endpoint-style-structure 2023-04-02 00:03:13 -04:00
Danny Avila
6cad5d5b50 Merge pull request #145 from danny-avila/fix-docker
fix docker issues with vite
2023-04-02 00:01:33 -04:00
Daniel Avila
8bc4d5e3fd refactor(Dockerfile): change COPY command to copy client side code from /client/dist instead of /client/public
refactor(Dockerfile): change port number from 3080 to 80
refactor(Dockerfile): remove unnecessary EXPOSE command
refactor(package.json): remove devDependencies for Vite and React plugin
2023-04-02 00:00:49 -04:00
Daniel Avila
b14726b2dd fix docker issues with vite 2023-04-01 23:59:16 -04:00
Daniel Avila
3121a3a6ba fix: add brackets to main property for dialogtemplate 2023-04-01 19:42:09 -04:00
Daniel Avila
8fa20d9356 Merge branch 'main' into feat-endpoint-style-structure 2023-04-01 19:41:36 -04:00
Daniel Avila
48b901fdfe Merge branch 'master' of https://github.com/danny-avila/chatgpt-clone into search 2023-04-01 19:13:54 -04:00
Danny Avila
c65a3b69ff Merge pull request #143 from danny-avila/DanO/use-vite-instead-of-webpack
feat: Use VITE in place of Webpack
2023-04-01 19:03:13 -04:00
Wentao Lyu
2cce2e30ba fix: change endpoint should not skip when endpoint same 2023-04-02 04:48:33 +08:00
Wentao Lyu
ad5fbc5fb1 merge Daniel Avila's commit
feat(ModelDropDown.jsx): add optional props to customize component appearance and behavior

feat(client): add ModelDropDown component to OpenAIOptions component
2023-04-02 04:40:13 +08:00
Daniel D Orlando
eb2d9bac33 refactor: have build served from dist folder 2023-04-01 13:28:37 -07:00
Wentao Lyu
45e17da241 feat: add preset and edit preset. 2023-04-02 04:15:07 +08:00
Wentao Lyu
80ef5008dd feat: add preset in server 2023-04-02 04:14:42 +08:00
Daniel D Orlando
6498ab79a4 build: install and configure Vite, move index.html 2023-04-01 12:58:49 -07:00
Daniel D Orlando
78dabe55ae refactor: rename tailwind config 2023-04-01 12:57:39 -07:00
Daniel D Orlando
64fc2e84f2 refactor: rename and fix postcss.config 2023-04-01 12:57:17 -07:00
Daniel D Orlando
ff757f27cf fix: fix files to adhere to standard conventions 2023-04-01 12:56:32 -07:00
Daniel Avila
2157eb8f30 feat(ModelDropDown.jsx): add optional props to customize component appearance and behavior
feat(client): add ModelDropDown component to OpenAIOptions component
2023-04-01 15:29:38 -04:00
Daniel Avila
7487b59de7 Merge branch 'feat-endpoint-style-structure' of https://github.com/danny-avila/chatgpt-clone into feat-endpoint-style-structure 2023-04-01 14:27:40 -04:00
Daniel Avila
efe55d8842 style(ui): replace SaveIcon with Save from lucide-react in EndpointOptionsPopover component 2023-04-01 14:27:34 -04:00
Wentao Lyu
d76efa7874 code cleanup 2023-04-02 00:57:27 +08:00
Daniel Avila
c5805d710b Merge branch 'feat-endpoint-style-structure' of https://github.com/danny-avila/chatgpt-clone into feat-endpoint-style-structure 2023-04-01 12:49:42 -04:00
Daniel Avila
f5ffa81455 complete: modeldropdown 2023-04-01 12:49:20 -04:00
Wentao Lyu
4d7fa26e6c feat: move EndpointOptionsPopover as a common component 2023-04-02 00:29:53 +08:00
Wentao Lyu
099210c10e feat: add default text.
fix: slider should set a number
2023-04-01 23:45:19 +08:00
Wentao Lyu
46e3ef4049 feat: endpoint setting mobile style.
feat: save and endpoint option works!
2023-04-01 23:27:44 +08:00
Wentao Lyu
edaf7c3ad1 feat: advanced style for openAI endpoint 2023-04-01 14:33:26 +08:00
Wentao Lyu
60be4f12b7 feat: return endpoint config from server 2023-04-01 14:33:07 +08:00
Daniel Avila
b687ab30ed style(OpenAIOptions): add dark mode support to Settings2 icon
feat(OpenAIOptions): pass isOpen prop to Settings component to toggle visibility
2023-03-31 21:51:06 -04:00
Daniel Avila
467e24c9ed style(Input/OpenAIOptions): comment out unused code and add gray color to settings icon 2023-03-31 21:48:27 -04:00
Daniel Avila
9098362377 style(Settings.jsx): remove unnecessary line break
style(Settings.jsx): add text-sm class to Label components
style(Settings.jsx): add flex, h-10, max-h-10, w-full, resize-none, px-3, py-2, focus:ring-0, focus:ring-offset-0, focus:ring-opacity-0, focus:outline-none classes to Input component
2023-03-31 21:45:29 -04:00
Daniel Avila
064dfb5336 fix(Settings.jsx): change min value of Slider component to -2 to allow negative values for presP variable 2023-03-31 21:11:43 -04:00
Daniel Avila
404566c1fb update more button and misc. styling 2023-03-31 18:38:58 -04:00
Daniel Avila
92dbdcaaa2 refactor(OptionHover.jsx): simplify types object by removing unnecessary nesting and description keys 2023-03-31 17:35:30 -04:00
Danny Avila
97e3ff6b6f reset package-lock 2023-03-31 16:19:48 -04:00
Danny Avila
b5541903e4 merge latest 2023-03-31 16:09:45 -04:00
Danny Avila
580d82ffe8 basic settings done for openAI 2023-03-31 16:08:52 -04:00
Wentao Lyu
5cb59885ec fix : endpoint option should hide on exist conversation 2023-04-01 03:02:16 +08:00
Wentao Lyu
ec47879edc clean: parentMassageId is no more needed in conversation. 2023-04-01 02:15:09 +08:00
Wentao Lyu
b67af67433 feat: support copy to clipboard
feat: move regenerate to all messages
feat: move stop generate to replace submit button
feat: make options' position more clear
2023-04-01 02:12:15 +08:00
Danny Avila
e8b2c2059d feat: select options 2023-03-31 13:12:06 -04:00
Wentao Lyu
bb1f8d731b feat: select model
feat: force to be advanced mode
2023-04-01 00:38:05 +08:00
Wentao Lyu
059006382d feat: add animation 2023-04-01 00:09:49 +08:00
Wentao Lyu
462660d554 feat: prototype of endpoint setting 2023-03-31 23:16:00 +08:00
Danny Avila
b703d3706b refactor(BingStyles.jsx): use cn utility function to concatenate class names in defaultSelected variable 2023-03-31 08:36:42 -04:00
Wentao Lyu
8b86fd0901 fix: jailbreak should be boolean 2023-03-31 16:36:24 +08:00
Wentao Lyu
a8e44d0028 feat: view endpoint setting in message header. 2023-03-31 16:35:30 +08:00
Wentao Lyu
babc4da7ab remove yarn.lock 2023-03-31 04:41:24 +08:00
Wentao Lyu
03f1a439d6 fix: suggestions seems not used. what's this? 2023-03-31 04:39:11 +08:00
Wentao Lyu
96639b82a8 feat: bingstyles now support endpoint 2023-03-31 04:33:26 +08:00
Wentao Lyu
e8e3903b78 feat: feat: new endpoint-style endpoint select
fix: a wrong use of bingai params
2023-03-31 04:22:16 +08:00
Wentao Lyu
adcc021c9e feat: feat: new endpoint-style submit 2023-03-31 03:22:57 +08:00
Wentao Lyu
089ca5f120 feat: new endpoint-style icon 2023-03-31 01:41:41 +08:00
Wentao Lyu
7e8b31cd09 fix: a typo in authenticatedOrRedirect 2023-03-31 00:26:43 +08:00
Wentao Lyu
c53778e7c1 feat: new endpoint-style structure in client side
NOT FINISHED. model menu and icon and send message not work.
2023-03-31 00:20:45 +08:00
Wentao Lyu
dd825dc6d4 feat: new endpoint-style structure in server side 2023-03-31 00:20:18 +08:00
Wentao Lyu
36c126e7a5 feat: show toneStyle in conversation title bar 2023-03-30 18:18:06 +08:00
Daniel Avila
9b04ffabca chore(client): update postcss-loader and postcss-preset-env packages to latest versions
chore(client): update webpack package to latest version
2023-03-29 20:17:17 -04:00
Danny Avila
cf2bab31cf reset packages and uninstall unused dep. 2023-03-29 14:12:24 -04:00
Danny Avila
d6fdf41011 fix: add text to global state 2023-03-29 11:25:27 -04:00
Danny Avila
79bb54db9c Merge pull request #140 from danny-avila/feat-refactor
Code refactoring
2023-03-29 11:00:04 -04:00
Danny Avila
4a94ee7af8 feat: allow default gpt api model before frontend customization 2023-03-29 10:17:24 -04:00
Danny Avila
39ff9c1bc2 fix: resize bing tabs 2023-03-29 09:02:49 -04:00
Danny Avila
b9699feb3b fix: prevent scroll to top on initial messages 2023-03-29 08:49:39 -04:00
Danny Avila
f93df2aea6 lift react markdown 2023-03-29 08:19:00 -04:00
Wentao Lyu
2c1871d5ba fix: update url rule in nginx 2023-03-29 16:11:43 +08:00
Wentao Lyu
e796a19136 fix: remove related messages when deleting conversations. 2023-03-29 13:32:01 +08:00
Daniel Avila
0d7300be9b fix: chatgptBrowser handling and ask.js refactor 2023-03-28 22:28:43 -04:00
Daniel Avila
005d8fb178 edit titleConvo for consistent results 2023-03-28 19:50:37 -04:00
Daniel Avila
f53b620df5 chore: add back clear convo dialog 2023-03-28 19:10:22 -04:00
Wentao Lyu
e706f0ea9e typo 2023-03-29 06:44:18 +08:00
Wentao Lyu
e663270072 fix: show message list. 2023-03-29 05:48:44 +08:00
Danny Avila
74924d2eea reset package-lock again 2023-03-28 16:29:42 -04:00
Danny Avila
a04ea81143 Merge branch 'feat-refactor' of https://github.com/danny-avila/chatgpt-clone into feat-refactor 2023-03-28 15:35:40 -04:00
Wentao Lyu
e818ee913d fix: add typescript package for react 2023-03-29 03:34:13 +08:00
Danny Avila
2a16a64612 fix: cursor appears on placeholder message 2023-03-28 15:32:22 -04:00
Danny Avila
95e9f05688 chore: reset package lock file 2023-03-28 15:15:10 -04:00
Wentao Lyu
ee3f6e1d1d remove react-redux.
help needed, please generate a package-lock.json
2023-03-29 02:42:37 +08:00
Wentao Lyu
b7af3595cf cleanup remove redux store 2023-03-29 02:38:24 +08:00
Wentao Lyu
aa26eea8c5 fix: missing icon of search result
feat: use search result message as single list
2023-03-29 02:29:15 +08:00
Danny Avila
c4be973b78 Update README.md 2023-03-28 14:12:08 -04:00
Wentao Lyu
dc743df255 feat: update title generator prompt, to support better on language. 2023-03-29 01:50:57 +08:00
Wentao Lyu
319e4f0f95 fix: set to default model in searchPlaceholderConversation
fix: set max auth cookie to 7 days
2023-03-29 01:26:58 +08:00
Wentao Lyu
f595cb2aa1 fix: three dot looks wide on android chrome. 2023-03-29 01:09:03 +08:00
Wentao Lyu
894aad9f0b sync with main 2023-03-29 00:20:57 +08:00
Wentao Lyu
5467d550e5 Merge remote-tracking branch 'origin/main' into feat-refactor-0.1.1 2023-03-29 00:19:46 +08:00
Wentao Lyu
d0d0a3d23e feat: print nothing found when no search result.
fix: handle 404 of conversation fetch failed
2023-03-29 00:18:27 +08:00
Wentao Lyu
370dc2dd8a feat: support search-style-url
fix: url can be null in conversationId and query
fix: get conversation api should handle not found.
2023-03-29 00:08:02 +08:00
Danny Avila
d9363b93c6 Create .github/FUNDING.yml 2023-03-28 11:48:47 -04:00
Danny Avila
b1b904ce5a Merge pull request #139 from danny-avila/fix-bugs
Fix bugs
2023-03-28 11:45:05 -04:00
Danny Avila
4564b648f7 fix: failsafe to prevent all convos clearing from delete button 2023-03-28 11:42:36 -04:00
Danny Avila
0fbbe74479 revert bing to last working state 2023-03-28 11:38:56 -04:00
Wentao Lyu
8ea98cca5d refactor: bing button
THIS IS NOT FINISHED. DONT USE THIS
2023-03-28 23:00:29 +08:00
Wentao Lyu
c7c30d8bb5 refactor: basic message and send message. as well as model
THIS IS NOT FINISHED. DONT USE THIS
2023-03-28 22:39:27 +08:00
Wentao Lyu
de8f519742 refactor: update package-lock 2023-03-28 20:37:34 +08:00
Wentao Lyu
af3d74b104 refactor: nav and search.
feat: use recoil to replace redux
feat: use react-native

THIS IS NOT FINISHED. DONT USE THIS
2023-03-28 20:36:21 +08:00
Danny Avila
2ad675196f Merge pull request #137 from danny-avila/fix-css
fix: set max height on customgpt prompt prefix
2023-03-28 08:11:43 -04:00
Danny Avila
e434b3afea fix: set max height on customgpt prompt prefix 2023-03-28 08:10:22 -04:00
Wentao Lyu
d8ccc5b870 fix: clearConvo will remove all messages 2023-03-28 01:19:44 +08:00
Wentao Lyu
7d43032a98 feat: return home page on any path
fix: clearConvo will remove all messages
2023-03-28 00:15:29 +08:00
Danny Avila
5b8483828b Merge pull request #136 from danny-avila/fix-css
fix: css matches official more closely with new markdown handling
2023-03-27 09:56:14 -04:00
Danny Avila
3c23f16b98 fix: css matches official more closely with new markdown handling 2023-03-27 09:55:24 -04:00
Danny Avila
7dc479e0a0 Merge pull request #135 from HyunggyuJang/follow-up/title-generation
updateConvo for title updating
2023-03-27 09:31:45 -04:00
Danny Avila
ee9419cb0b Merge pull request #133 from HyunggyuJang/refactor/markdown
Use react markdown as default, cleanup dependencies
2023-03-27 09:31:32 -04:00
Danny Avila
1c26bbe43e Merge pull request #128 from HyunggyuJang/sydney/tone-adjustment
Make tone adjustment available after conversation started if sydney
2023-03-27 09:31:16 -04:00
Danny Avila
52d57f67aa Merge pull request #127 from HyunggyuJang/bing/fast-tone
Add fast tab for bing tone
2023-03-27 09:31:03 -04:00
Hyunggyu Jang
7486a56816 updateConvo for title updating 2023-03-27 12:53:40 +09:00
Hyunggyu Jang
40e23b013a Use react markdown as default, cleanup dependencies 2023-03-27 10:22:37 +09:00
Hyunggyu Jang
c119c4044a Adjust color 2023-03-26 19:52:47 +09:00
Hyunggyu Jang
6f037309ad Fix for mobile view 2023-03-26 13:49:55 +09:00
Hyunggyu Jang
af4110ff15 Do not allow to override bing AI tone in the middle of conversation 2023-03-26 13:25:29 +09:00
Hyunggyu Jang
55f04ffa60 Make tone adjustment available after conversation started if sydney 2023-03-26 13:12:48 +09:00
Hyunggyu Jang
e5cf51b2d6 Add fast tab for bing tone 2023-03-26 13:10:36 +09:00
Danny Avila
e8e512a451 Merge pull request #126 from danny-avila/minor-updates
chore: minor updates
2023-03-25 10:21:41 -04:00
Daniel Avila
c2967eafa4 chore: update docker-compose for interchangeable env for search, uninstalled unused deps 2023-03-25 10:20:39 -04:00
Danny Avila
853b4dfd49 Merge pull request #125 from danny-avila/upgrade-node
Upgrade node
2023-03-25 10:02:17 -04:00
Danny Avila
e49adaa314 Merge pull request #124 from danny-avila/bing-styles
Bing styles
2023-03-25 10:01:19 -04:00
Daniel Avila
394cdcd9f4 fix: remove 'class=' error 2023-03-25 09:55:33 -04:00
Daniel Avila
c5561434c8 lift node-api library 2023-03-25 09:53:39 -04:00
Daniel Avila
26e7a715e0 feat: complete bing styles (bing is passed tone style) 2023-03-25 09:40:36 -04:00
Daniel Avila
b07b74ba54 feat: complete bing styles (view) 2023-03-25 09:34:00 -04:00
Danny Avila
70152174b9 Merge pull request #123 from HyunggyuJang/fix/unknown-lang
Fix Unknown language bug
2023-03-25 08:39:10 -04:00
Hyunggyu Jang
8bd29f6d98 Fix Unknown language bug 2023-03-25 18:40:12 +09:00
Daniel Avila
b8720eec3d feat: dark mode for style tabs 2023-03-24 19:02:42 -04:00
Daniel Avila
ad0f2408c8 Merge branch 'main' into bing-styles 2023-03-24 18:50:56 -04:00
Danny Avila
e2ad9accbe Merge pull request #122 from danny-avila/fix-search
Fix search
2023-03-24 18:45:03 -04:00
Daniel Avila
b496174b4c fix: correctly escapes user content in search results 2023-03-24 18:44:12 -04:00
Daniel Avila
3295eb806c chore: expose mongodb on port 27018 2023-03-24 18:27:51 -04:00
Daniel Avila
34bef48e84 fix: correctly searches bing messages 2023-03-24 18:26:59 -04:00
Danny Avila
a6c93ad681 dark mode in progress 2023-03-24 16:31:27 -04:00
Danny Avila
89ab74a913 feat: complete frontend/backend tone handling 2023-03-24 16:21:10 -04:00
Danny Avila
a46ec62532 Update README.md 2023-03-24 16:19:42 -04:00
Danny Avila
22a967927f Update README.md 2023-03-24 16:19:16 -04:00
Danny Avila
0aa7581358 Add files via upload
in-depth local instructions thanks to @fuegovic
2023-03-24 16:17:04 -04:00
Danny Avila
513cd28528 Update README.md 2023-03-24 16:16:06 -04:00
Danny Avila
83b88bd759 feat: complete view for styles 2023-03-24 14:46:07 -04:00
Danny Avila
4f18a471b0 Merge pull request #121 from danny-avila/fix-clear-mobile
fix: clear convos on mobile now has top z-index
2023-03-24 08:20:30 -04:00
Danny Avila
737abd54ac Merge pull request #120 from HyunggyuJang/refactor/sydney
Refactor sydney & fix error
2023-03-24 08:19:06 -04:00
Hyunggyu Jang
1de51467ec Fix forgetfulness for bing/sydney 2023-03-24 12:48:20 +09:00
Hyunggyu Jang
dcc0ab98e1 refactor: Remove verbose user message update for sydney 2023-03-24 12:27:46 +09:00
Daniel Avila
730256dcda fix: clear convos on mobile now has top z-index 2023-03-23 23:18:36 -04:00
Danny Avila
40e793477b Update README.md 2023-03-23 16:30:58 -04:00
Danny Avila
b856db4772 Update .env.example
fix typo
2023-03-23 16:26:22 -04:00
Danny Avila
cab1cbceab Merge pull request #118 from danny-avila/fix-auth
fix: auth env var must have no value, as well as assigned sample_user in route
2023-03-23 15:41:58 -04:00
Danny Avila
b73be0dcfa fix: auth env var must have no value, as well as assigned username incase a falsy value is set 2023-03-23 15:37:25 -04:00
Danny Avila
c6fb3018e7 Merge pull request #117 from danny-avila/search-final
Search final
2023-03-23 13:37:55 -04:00
Danny Avila
95cf27ee3e edit env example 2023-03-23 13:30:55 -04:00
Danny Avila
7afe09fa02 chore: clear timeouts 2023-03-23 13:16:07 -04:00
Danny Avila
bff33c79b3 merge latest pr from main 2023-03-23 13:04:40 -04:00
Danny Avila
f73936e5f4 Merge branch 'main' into search-final 2023-03-23 13:02:52 -04:00
Danny Avila
2b8d37c38f edit env example 2023-03-23 12:45:42 -04:00
Danny Avila
ca3da2505a edit env example 2023-03-23 11:49:54 -04:00
Danny Avila
d3046dca07 Merge pull request #115 from HyunggyuJang/sydney/branching
Enable branching (edit message) for Sydney
2023-03-23 11:38:44 -04:00
Danny Avila
25fd39c2b9 Update README.md
organize readme for readability
2023-03-23 11:35:23 -04:00
Daniel Avila
ab8724b568 edit docker-compose for meilisearch 2023-03-22 22:35:25 -04:00
Hyunggyu Jang
c240d14864 Enable branching (edit message) for Sydney 2023-03-23 11:27:14 +09:00
Daniel Avila
1d464fdcfa reduce noisy meili errors if not set 2023-03-22 21:23:01 -04:00
Daniel Avila
aacc292522 Merge branch 'main' into search-final 2023-03-22 20:57:20 -04:00
Danny Avila
350a1bbae0 Update README.md 2023-03-22 20:48:47 -04:00
Danny Avila
90b74aff2e Merge pull request #104 from HyunggyuJang/refactor/docker
Refactor: merge docker setup file into one dockerfile & one docker-compose.yml
2023-03-22 20:13:10 -04:00
Daniel Avila
37f36ec44a chore: update meilisearch compose 2023-03-22 20:12:38 -04:00
Daniel Avila
71b7eaa3f5 chore: update env example and add browser client config 2023-03-22 20:09:52 -04:00
Daniel Avila
719413f87a feat: syncs across document deletions 2023-03-22 19:53:09 -04:00
Daniel Avila
97634865eb feat: syncs across document deletions 2023-03-22 19:52:38 -04:00
Daniel Avila
1dbfb0dab7 minor styling changes, cache queried messages on server 2023-03-22 18:26:29 -04:00
Daniel Avila
0a671849b5 feat: clearing convos requires confirmation 2023-03-22 17:51:51 -04:00
Daniel Avila
655e7ce6d6 chore: improve meili error handling 2023-03-22 17:15:32 -04:00
Danny Avila
68979015c1 markdown styling changes in progress 2023-03-22 16:31:57 -04:00
Danny Avila
8f58c95452 feat: main styling/ui/ux final changes 2023-03-22 16:06:11 -04:00
Danny Avila
67161c983f chore: meilisearch setup config 2023-03-22 10:23:55 -04:00
Danny Avila
73449d9ec6 feat: build tree by convoId 2023-03-22 10:23:36 -04:00
HyunggyuJang
f5d102b7bd Update docker-compose.yml 2023-03-22 23:20:03 +09:00
Hyunggyu Jang
40ed6fa9ec Provide nginx docker build recipe 2023-03-22 22:59:29 +09:00
Danny Avila
5164cf46ac chore: error handling for complete omission of env var 2023-03-22 09:38:38 -04:00
Daniel Avila
277685c218 search: updating search endpoint (wip) 2023-03-22 01:34:36 -04:00
Daniel Avila
e25aa74d7b search: correctly register/export schema/models, also made IIFE 2023-03-22 01:33:49 -04:00
Daniel Avila
47a6cfcafd chore: error controller (wip) 2023-03-22 01:32:55 -04:00
Daniel Avila
83a96706b4 chore: add prettier 2023-03-22 01:32:30 -04:00
Daniel Avila
75be4d9722 search: helper fn for invalid convoId strings 2023-03-22 01:31:49 -04:00
Daniel Avila
a5cf2f9148 search: correctly register/export schema/models for mongoMeili 2023-03-22 01:31:01 -04:00
Daniel Avila
8be19f9982 search: sync on offset between meili and mongo 2023-03-22 01:30:04 -04:00
Hyunggyu Jang
36f3d37ecc Remove nginx setting 2023-03-22 10:50:59 +09:00
Hyunggyu Jang
c233cc0d5c Move Dockerfiles into one toplevel Dockerfile 2023-03-22 10:50:44 +09:00
Daniel Avila
194051e424 feat: api will plugin mongoMeili without a valid connection 2023-03-21 20:06:27 -04:00
Daniel Avila
94c0fbb525 feat: api will disable search if no meilisearch connection 2023-03-21 19:44:31 -04:00
Daniel Avila
97a6cd801b feat: simple api call to enable search 2023-03-21 19:31:57 -04:00
Danny Avila
1041146fcb Merge pull request #108 from danny-avila/upgrade-fix
fix: chatgpt now using latest
2023-03-21 17:41:05 -04:00
Danny Avila
e531a17e0f fix: chatgpt now using latest 2023-03-21 16:32:32 -04:00
Danny Avila
30a7a80bfc Merge pull request #107 from danny-avila/chores
Chores
2023-03-21 14:27:00 -04:00
Danny Avila
67f8374c9e chore: eslint rules and blinker update in text handling 2023-03-21 13:41:31 -04:00
Danny Avila
0cc4aea204 chore: reorg. content files, add blinking cursor 2023-03-21 09:46:08 -04:00
Danny Avila
04796824d5 chore: init eslint 2023-03-21 08:48:35 -04:00
Danny Avila
9020239e1f Merge pull request #103 from HyunggyuJang/use-lock-file
Use npm ci rather than install to be consistent with lock file
2023-03-21 08:31:09 -04:00
Hyunggyu Jang
0a12b47760 Use npm ci rather than install to be consistent with lock file 2023-03-21 12:23:25 +09:00
Danny Avila
9358a4fdb5 Merge pull request #102 from danny-avila/bing-hotfix
bing hotfix, latest api, uses sydney
2023-03-20 17:16:45 -04:00
Daniel Avila
7d796f2c3e bing hotfix, latest api, uses sydney 2023-03-20 17:02:37 -04:00
Danny Avila
0a1651f6a1 Update README.md 2023-03-20 04:44:42 -04:00
Daniel Avila
d13315c45b chore: comment out auth route 2023-03-20 02:58:41 -04:00
Daniel Avila
0b75d5d6fe chore: another hotfix patch on getConvoTitle 2023-03-20 02:30:08 -04:00
Wentao Lyu
39819b744c doc: add magic of generate auth code 2023-03-20 02:22:04 -04:00
Daniel Avila
28c8f066d9 chore: add latest api as dep with alias 2023-03-20 02:18:03 -04:00
Daniel Avila
c85602b93b chore: Replace hard coded message ID with unique one 2023-03-20 01:51:07 -04:00
Daniel Avila
08c91871c7 chore: hotfix: browser will not leave empty convo 2023-03-20 01:42:06 -04:00
Daniel Avila
80ca3bc375 chore: hotfix for browser client 2023-03-20 01:35:02 -04:00
HyunggyuJang
0405206438 Remove crypto from client side
Unless, if we tries to access the client side page from http protocol, crypto.randomUUID() isn't available.
2023-03-20 00:58:17 -04:00
Daniel Avila
0af8f6a699 chore: fix broken browser client 2023-03-20 00:51:56 -04:00
Daniel Avila
b0936fa322 chore: unplug meilisearch, add leading option to throttle 2023-03-20 00:48:16 -04:00
Daniel Avila
4cd0ff2682 fix: throttle scroll to bottom 2023-03-19 11:45:03 -04:00
Daniel Avila
4ce60537ca search result styling changes 2023-03-19 11:25:12 -04:00
Daniel Avila
0b47218cd5 markdown library change 2023-03-19 01:14:19 -04:00
Daniel Avila
d56aa2edef loads up to 20 messages, debugging markdown issue 2023-03-18 23:18:36 -04:00
Daniel Avila
4e6168d8fa setup message population on search 2023-03-18 18:40:53 -04:00
Daniel Avila
4197a92609 feat: search working as expected 2023-03-18 17:49:24 -04:00
Daniel Avila
da42d6272a add loading state for fetching 2023-03-18 15:59:59 -04:00
Daniel Avila
b97594c000 fix: conflicting fetch with /api/convos 2023-03-18 14:28:10 -04:00
Daniel Avila
0f54ffd8b4 feat: search bar working, still in progress 2023-03-18 01:40:49 -04:00
Daniel Avila
610cba4a60 backend logic drafted, moving to frontend 2023-03-17 22:20:36 -04:00
Daniel Avila
4f5ee8b198 move db functions 2023-03-17 19:58:13 -04:00
Daniel Avila
586c162404 merge commit 2023-03-17 19:40:44 -04:00
Danny Avila
1308ef1394 Merge pull request #88 from wtlyu/feat-clean-after-regenerate-pr
Feat clean after regenerate pr
2023-03-17 15:31:32 -04:00
Danny Avila
1513c27f7d adjust: wording and placeholder text size 2023-03-17 14:48:21 -04:00
Wentao Lyu
0ff3bbb28f clean up 2023-03-18 02:15:24 +08:00
Wentao Lyu
7987c0100c fix: add crypto as deps. [need confirm] 2023-03-18 02:04:51 +08:00
Wentao Lyu
e8611a1d07 fix: don't reset isSubmitting in useEffect of submission 2023-03-18 01:54:06 +08:00
Wentao Lyu
ce78123369 fix: allow delete of last char while submitting 2023-03-18 01:24:35 +08:00
Wentao Lyu
a90db1f1a4 fix: add model to customGpts
fix: filter models by model only
2023-03-18 01:12:45 +08:00
Wentao Lyu
a213868b17 fix: set isSubmitting with messages together
style: some notification
2023-03-18 01:12:45 +08:00
Wentao Lyu
6b2a2bb858 feat: save cancelled flag in message 2023-03-18 01:11:44 +08:00
Wentao Lyu
fea3afa740 fix: missing import 2023-03-18 01:10:20 +08:00
Wentao Lyu
7372b37fe6 style: hide footer in mobile mode
style: avoid ui overflow in mobile mode
2023-03-18 01:09:59 +08:00
Danny Avila
e11ce141d7 Merge pull request #86 from danny-avila/fix-scroll
fix: fix weird scrolling behavior on last message
2023-03-17 12:38:15 -04:00
Danny Avila
46fbd3b66a fix: fix weird scrolling behavior on last message 2023-03-17 12:34:54 -04:00
Danny Avila
ce3f03267a Merge pull request #80 from wtlyu/feat-regenerate-and-cancel
Feat regenerate and cancel
2023-03-17 10:40:45 -04:00
Danny Avila
9a2392e4d5 fix: mobile styling after regen buttons 2023-03-17 22:20:23 +08:00
Danny Avila
1eab4d240d Merge pull request #85 from danny-avila/bing-refusal
Bing refusal
2023-03-17 09:14:40 -04:00
Danny Avila
5568a60174 handle bing message refusal 2023-03-17 09:11:45 -04:00
Danny Avila
ea4180f22a remove detect code (not always accurate) 2023-03-17 09:11:31 -04:00
Daniel Avila
6d2f3361d0 feat: search in progress 2023-03-16 21:20:40 -04:00
Daniel Avila
9995a159aa feat: reorganize api files, add mongoMeili 2023-03-16 19:38:16 -04:00
Daniel Avila
854f1c3572 feat: search, refactoring messages model 2023-03-16 17:20:26 -04:00
Danny Avila
dcc13daf67 testing: mongo meilisearch 2023-03-16 16:22:08 -04:00
Wentao Lyu
2310bab348 revert: dont overwrite the mobile input panel color 2023-03-17 03:54:15 +08:00
Wentao Lyu
d64edfdc7d feat: add chrome title color 2023-03-17 03:50:34 +08:00
Wentao Lyu
a8c53f1f0d mobile style, for input panel and regenerate buttons 2023-03-17 03:50:34 +08:00
Wentao Lyu
ef9f1ee1cf feat: cancellable api request 2023-03-17 03:50:34 +08:00
Wentao Lyu
66ad54168a feat: regenerate for bingai 2023-03-17 03:50:34 +08:00
Wentao Lyu
0891566d1e feat: add regenerate to all response message as official 2023-03-17 03:49:59 +08:00
Wentao Lyu
e3b0cb7db7 feat: show model at the top of conversation messages. 2023-03-17 03:48:45 +08:00
Danny Avila
c27554ed2e fix: allow customs in model filter 2023-03-16 15:05:23 -04:00
Danny Avila
87f793f1c4 Merge pull request #79 from danny-avila/improve-titles
fix: add deterministic titling
2023-03-16 15:03:04 -04:00
Danny Avila
4078c5283b fix: add deterministic titling 2023-03-16 15:02:40 -04:00
Danny Avila
5cac7e48f0 Merge pull request #74 from wtlyu/feat-model-based-on-key
feat: show model based on configured Keys
2023-03-16 15:00:23 -04:00
Danny Avila
7a08c77850 Update README.md 2023-03-16 14:11:24 -04:00
Danny Avila
1fe9e29187 Merge branch 'master' into feat-model-based-on-key 2023-03-16 13:39:41 -04:00
Danny Avila
ba8692dbe4 Merge pull request #59 from wtlyu/feat-usersys
Feat usersys
2023-03-16 13:35:03 -04:00
Danny Avila
d30b406c4c Merge pull request #4 from wtlyu/revisions
Revisions
2023-03-16 13:34:36 -04:00
Danny Avila
915cda70ef fix: user:user redundancies 2023-03-16 13:32:04 -04:00
Danny Avila
df19595c5b fix: icon and mobile textarea styling 2023-03-16 13:30:10 -04:00
Danny Avila
867b3073d4 fix: icon and mobile textarea styling 2023-03-16 13:29:13 -04:00
Danny Avila
e4e28dbbe2 Update README.md 2023-03-16 10:57:44 -04:00
Danny Avila
cdbc0e21e7 Update README.md 2023-03-16 10:57:18 -04:00
Wentao Lyu
b6f7f95709 feat: set default model once model list read. 2023-03-16 21:12:33 +08:00
Wentao Lyu
131af50034 feat: show model based on configured Keys 2023-03-16 14:44:14 +08:00
Wentao Lyu
23c050b54e style: mobile style of landing 2023-03-16 14:07:48 +08:00
Wentao Lyu
7442294c41 fix: mobile sizing style of icon 2023-03-16 14:05:12 +08:00
Wentao Lyu
a47dbe6262 feat: use same icon style in TextChat 2023-03-16 13:45:46 +08:00
Wentao Lyu
aabb19656e feat: combine customgpt to user 2023-03-16 13:30:20 +08:00
Wentao Lyu
b0284b6974 sync updates and merge with feat-resubmit 2023-03-16 13:20:54 +08:00
Wentao Lyu
62d88380e0 feat: add sample multi-user support
feat: update README
2023-03-16 13:20:34 +08:00
Daniel Avila
41f351786f fix: oversight, artifact in bing route 2023-03-15 22:36:16 -04:00
Danny Avila
ffcfb69dee Merge pull request #72 from danny-avila/fix-mobile-switch
fix: mobile view for sibling switch
2023-03-15 19:06:41 -04:00
Daniel Avila
c91ce36227 fix: mobile view for sibling switch 2023-03-15 19:05:17 -04:00
Danny Avila
d06e58f043 Merge pull request #55 from wtlyu/feat-resubmit
Feature: multipath message & resubmit, conversation placeholder, separate title generation, and some bug fix.
2023-03-15 18:06:35 -04:00
Daniel Avila
d052d221dc chore: switch focus to textarea when custom model change (still need to figure out reg model change) 2023-03-15 18:05:34 -04:00
Danny Avila
ff45511011 Merge pull request #3 from wtlyu/final-adj
Final adjustments
2023-03-15 17:08:18 -04:00
Danny Avila
8c6340aed0 chore: refactor cursor blink, debugging 2023-03-15 16:38:01 -04:00
Danny Avila
84b104e65f chore: delegate response text parsing to one location 2023-03-15 15:44:48 -04:00
Danny Avila
a0c94715ce chore: refactor titleConvo 2023-03-15 15:21:04 -04:00
Danny Avila
a8aad30fc8 chore: memoized Messages component, will require custom equality check 2023-03-15 14:36:17 -04:00
Danny Avila
2fd50c99b8 fix: debounce title request and handle error with default title 2023-03-15 12:47:30 -04:00
Danny Avila
96ca783517 chore: re-organize message modules, fix icon size, convo reset properly rebuilds Tree 2023-03-15 10:42:45 -04:00
Wentao Lyu
45ca0a8713 fix: gptCustom icon should show as same in model and message 2023-03-15 14:42:39 +08:00
Wentao Lyu
5d0b849930 feat: show icon within model select menu
fix: use icon for gptCustom
2023-03-15 14:21:08 +08:00
Wentao Lyu
54aa9debb4 fix: don't reset new convo if model not change
fix: change model will clear all messages.
2023-03-15 13:38:01 +08:00
Daniel Avila
4e91437049 fix: convo resets, sets new Convo 2023-03-14 21:32:25 -04:00
Daniel Avila
918f2fecb6 fix: convo resets on model change 2023-03-14 21:25:02 -04:00
Daniel Avila
796d8031e8 fix: ensure custom params are not passed to non custom models 2023-03-14 20:21:41 -04:00
Daniel Avila
6e32f71565 fix: adjust custom client for new progress CB 2023-03-14 20:15:06 -04:00
Daniel Avila
6192c2964e fix: validation to avoid saving customGpt params to non-custom models 2023-03-14 20:14:38 -04:00
Daniel Avila
626a8fbd8e fix: if db is empty, will never show new convos until refresh 2023-03-14 18:53:46 -04:00
Danny Avila
a8344ec5bf Merge pull request #2 from wtlyu/refactors
Refactors
2023-03-14 18:16:44 -04:00
Danny Avila
c230fe41f4 Merge branch 'feat-resubmit' into refactors 2023-03-14 16:09:25 -04:00
Danny Avila
d0ef0f84c8 chore: delegate text handling to one place, html sanitization in progress 2023-03-14 16:05:46 -04:00
Wentao Lyu
8289558d94 feat: pagination in nav 2023-03-15 04:05:14 +08:00
Danny Avila
2e20b28c4d chore: refactor progressCB to one place, fix sydney, and sanitize html 2023-03-14 15:42:59 -04:00
Danny Avila
9a17e94f8f fix: refactor migration and sort old convos correctly 2023-03-14 14:51:26 -04:00
Wentao Lyu
71fc86b9a6 fix: buildTree should store parent-not-exist message as root. rather than dropping them. 2023-03-15 02:43:21 +08:00
Wentao Lyu
8882432210 fix: hide the edit button when bingai 2023-03-15 02:33:08 +08:00
Wentao Lyu
644f3f716f feat: re-orginazed three ask api. To provide ability to reproduce message.
feat: bing and sydney come to work again, [need more test]
2023-03-15 02:15:46 +08:00
Wentao Lyu
8b00805d24 fix: dont send gen_title twice 2023-03-15 02:15:46 +08:00
Wentao Lyu
d73375958b feat: return error as a error message, not only text 2023-03-15 02:15:46 +08:00
Wentao Lyu
7168498543 fix: jailbreakConversationId=false response will be saved jailbreakConversationId='false' in database. 2023-03-15 02:15:46 +08:00
Wentao Lyu
27515cb00a fix: generate title by backend 2023-03-15 02:15:46 +08:00
Daniel Avila
3e7ce67609 fix: migrate old schema to new 2023-03-15 02:15:46 +08:00
Daniel Avila
4fd05e15b4 fix: migrate old schema to new 2023-03-15 02:15:46 +08:00
Wentao Lyu
0fa19bb6ad feat: save error message into database. 2023-03-15 02:15:46 +08:00
Wentao Lyu
d9e5464b3b fix: cleanup debug msg 2023-03-15 02:15:46 +08:00
Wentao Lyu
953c5fc970 fix: w<1024px, will overflow. 2023-03-15 02:15:46 +08:00
Wentao Lyu
2afbc5883f fix: use onCompositionStart and onCompositionEnd to aviod enter submit when using input method. 2023-03-15 02:15:46 +08:00
Wentao Lyu
953f846958 fix: loading and send button, mobile style
feat: sibling switch, mobile style
fix: only the real submitting message will blink
feat: drop the text version username, use a similar square. (or it will mass up the sibling switch)
2023-03-15 02:15:46 +08:00
Wentao Lyu
a4d5f6a3f2 feat: fully multipath and resubmit 2023-03-15 02:15:46 +08:00
Wentao Lyu
4a39965b22 fix: add proxy to titleConvo 2023-03-15 02:15:46 +08:00
Wentao Lyu
90dc171b34 test: generate of title shouldn't be mislead by answer 2023-03-15 02:15:46 +08:00
Wentao Lyu
0e98cb4206 fix: in mobile view, resubmit edit button should always visible 2023-03-15 02:15:46 +08:00
Wentao Lyu
9f8e9cb091 feat: gen title by sperate api call
feat:

fix: rename of convo should based on real request.
2023-03-15 02:15:46 +08:00
Wentao Lyu
8773878be2 feat: create conversation at the beginning then return the userMessage 2023-03-15 02:15:46 +08:00
Wentao Lyu
5a409ccfa6 fix: don't resubmit html label
fix: hide the resubmit editor border
2023-03-15 02:15:46 +08:00
Wentao Lyu
0ed8a40a41 feat: merge all message.id into message.messageId
feat: the first message will have a parentMessageId as 00000000-0000-0000-0000-000000000000 (in order not to create new convo when resubmit)
feat: ask will return the userMessage as well, to send back the messageId

TODO: comment out the title generation.
TODO: bing version need to be test

fix: never use the same messageId
fix: never delete exist messages
fix: connect response.parentMessageId to the userMessage.messageId
fix: set default convo title as new Chat
2023-03-15 02:15:46 +08:00
Wentao Lyu
be71140dd4 fix: new message should append to the exist one 2023-03-15 02:15:46 +08:00
Wentao Lyu
6d51ec3e37 feat: auto hide edit button when edit is enabled. 2023-03-15 02:15:46 +08:00
Wentao Lyu
bdfc895800 feat: support resubmit.
TODO: basic implementation. should add multi-path record in future.

feat: add deleteMessahesSince
feat: saveMessage will do createOrSave
feat: reorginazed submission
2023-03-15 02:15:46 +08:00
Wentao Lyu
b9975ac283 fix: missing setSubmission 2023-03-15 02:15:46 +08:00
Wentao Lyu
dd1f74da72 replace all created to timestamps: true in db 2023-03-15 02:15:46 +08:00
314 changed files with 46688 additions and 32299 deletions

3
.dockerignore Normal file
View File

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

114
.eslintrc.js Normal file
View File

@@ -0,0 +1,114 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
commonjs: true,
es6: true
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
plugins: ['react', 'react-hooks', '@typescript-eslint'],
rules: {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }],
indent: ['error', 2, { SwitchCase: 1 }],
'max-len': [
'error',
{
code: 150,
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreComments: true
}
],
'linebreak-style': 0,
// "arrow-parens": [2, "as-needed", { requireForBlockBody: true }],
// 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
'no-console': 'off',
'import/extensions': 'off',
'no-use-before-define': [
'error',
{
functions: false
}
],
'no-promise-executor-return': 'off',
'no-param-reassign': 'off',
'no-continue': 'off',
'no-restricted-syntax': 'off',
'react/prop-types': ['off'],
'react/display-name': ['off']
},
overrides: [
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'no-unused-vars': 'off', // off because it conflicts with '@typescript-eslint/no-unused-vars'
'react/display-name': 'off',
'@typescript-eslint/no-unused-vars': 'warn'
}
},
{
files: ['rollup.config.js', '.eslintrc.js', 'jest.config.js'],
env: {
node: true
}
},
{
files: [
'**/*.test.js',
'**/*.test.jsx',
'**/*.test.ts',
'**/*.test.tsx',
'**/*.spec.js',
'**/*.spec.jsx',
'**/*.spec.ts',
'**/*.spec.tsx',
'setupTests.js'
],
env: {
jest: true,
node: true
},
rules: {
'react/display-name': 'off',
'react/prop-types': 'off',
'react/no-unescaped-entities': 'off'
}
},
{
files: '**/*.+(ts)',
parser: '@typescript-eslint/parser',
parserOptions: {
project: './client/tsconfig.json'
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended'
]
}
],
settings: {
react: {
createClass: 'createReactClass', // Regex for Component Factory to use,
// default to "createReactClass"
pragma: 'React', // Pragma to use, default to "React"
fragment: 'Fragment', // Fragment to use (may be a property of <pragma>), default to "Fragment"
version: 'detect' // React version. "detect" automatically picks the version you have installed.
}
}
};

13
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
# These are supported funding model platforms
github: [danny-avila]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

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

@@ -0,0 +1,41 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/api" # Location of package manifests
schedule:
interval: "weekly"
allow:
# Allow both direct and indirect updates for all packages
- dependency-type: "all"
commit-message:
prefix: "npm api prod"
prefix-development: "npm api dev"
include: "scope"
- package-ecosystem: "npm" # See documentation for possible values
directory: "/client" # Location of package manifests
schedule:
interval: "weekly"
allow:
# Allow both direct and indirect updates for all packages
- dependency-type: "all"
commit-message:
prefix: "npm client prod"
prefix-development: "npm client dev"
include: "scope"
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
allow:
# Allow both direct and indirect updates for all packages
- dependency-type: "all"
commit-message:
prefix: "npm all prod"
prefix-development: "npm all dev"
include: "scope"

72
.github/playwright.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
name: Playwright Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
tests_e2e:
name: Run end-to-end tests
timeout-minutes: 60
runs-on: ubuntu-latest
env:
BINGAI_TOKEN: ${{ secrets.BINGAI_TOKEN }}
CHATGPT_TOKEN: ${{ secrets.CHATGPT_TOKEN }}
MONGO_URI: ${{ secrets.MONGO_URI }}
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- name: Cache API dependencies
uses: actions/cache@v2
with:
path: ./api/node_modules
key: api-${{ runner.os }}-node-${{ hashFiles('./api/package-lock.json') }}
restore-keys: |
api-${{ runner.os }}-node-
- name: Install API dependencies
working-directory: ./api
run: npm ci
- name: Cache Client dependencies
uses: actions/cache@v2
with:
path: ./client/node_modules
key: client-${{ runner.os }}-node-${{ hashFiles('./client/package-lock.json') }}
restore-keys: |
client-${{ runner.os }}-node-
- name: Install Client dependencies
working-directory: ./client
run: npm ci
- name: Build Client
working-directory: ./client
run: npm run build
- name: Install global dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Start API server
working-directory: ./api
run: |
npm run start &
sleep 10 # Wait for the server to start
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: e2e/playwright-report/
retention-days: 30

28
.github/wip-playwright.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
tests_e2e:
name: Run end-to-end tests
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: e2e/playwright-report/
retention-days: 30

16
.gitignore vendored
View File

@@ -2,6 +2,7 @@
# Logs
data-node
meili_data
logs
*.log
@@ -33,6 +34,7 @@ client/public/main.js.LICENSE.txt
# Deployed apps should consider commenting these lines out:
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
node_modules/
meili_data/
api/node_modules/
client/node_modules/
bower_components/
@@ -44,12 +46,22 @@ bower_components/
.flooignore
# Environment
.npmrc
.env
cache.json
api/data/
.eslintrc.js
owner.yml
archive
.vscode/settings.json
src/style - official.css
/e2e/specs/.test-results/
/e2e/playwright-report/
/playwright/.cache/
.DS_Store
*.code-workspace
.idea
# meilisearch
meilisearch
data.ms/*
src/style - official.css

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

136
CHANGELOG.md Normal file
View File

@@ -0,0 +1,136 @@
# # 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.
⚠️ **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.
⚠️ **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.
For discussion and suggestion you can join us: **[community discord server](https://discord.gg/NGaa9RPCft)**
</details>
<details>
<summary><strong>2023-04-05</strong></summary>
**Released [v0.3.0](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.3.0)**, Introducing more customization for both OpenAI & BingAI conversations! This is one of the biggest updates yet and will make integrating future LLM's a lot easier, providing a lot of customization features as well, including sharing presets! Please feel free to share them in the **[community discord server](https://discord.gg/NGaa9RPCft)**
</details>
<details>
<summary><strong>2023-03-23</strong></summary>
**Released [v0.1.0](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.1.0)**, **searching messages/conversations is live!** Up next is more custom parameters for customGpt's. Join the discord server for more immediate assistance and update: **[community discord server](https://discord.gg/NGaa9RPCft)**
</details>
<details>
<summary><strong>2023-03-22</strong></summary>
**Released [v0.0.6](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.0.6)**, the latest stable release before **Searching messages** goes live tomorrow. See exact updates to date in the tag link. By request, there is now also a **[community discord server](https://s
</details>
<details>
<summary><strong>2023-03-20</strong></summary>
**Searching messages** is almost here as I test more of its functionality. There've been a lot of great features requested and great contributions and I will work on some soon, namely, further customizing the custom gpt params with sliders similar to the OpenAI playground, and including the custom params and system messages available to Bing.
The above features are next and then I will have to focus on building the **test environment.** I would **greatly appreciate** help in this area with any test environment you're familiar with (mocha, chai, jest, playwright, puppeteer). This is to aid in the velocity of contributing and to save time I spend debugging.
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>
##
## [Go Back to ReadMe](README.md)

26
CONTRIBUTORS.md Normal file
View File

@@ -0,0 +1,26 @@
# Contributors List
We appreciate all the contributors who helped make this project possible:
- danny-avila (Admin)
- wtlyu (Contributor)
- danorlando (Contributor)
- alfredo-f (Contributor)
- HyunggyuJang (Contributor)
- fuegovic (Contributor)
- DavidDev1334
- 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)

32
Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
# Base node image
FROM node:19-alpine AS base
WORKDIR /api
COPY /api/package*.json /api/
WORKDIR /client
COPY /client/package*.json /client/
WORKDIR /
COPY /package*.json /
RUN npm ci
# React client build
FROM base AS react-client
WORKDIR /client
COPY /client/ /client/
ENV NODE_OPTIONS="--max-old-space-size=2048"
RUN npm run build
# Node API setup
FROM base AS node-api
WORKDIR /api
COPY /api/ /api/
COPY --from=react-client /client/dist /client/dist
EXPOSE 3080
ENV HOST=0.0.0.0
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
COPY client/nginx.conf /etc/nginx/conf.d/default.conf
ENTRYPOINT ["nginx", "-g", "daemon off;"]

35
Dockerfile-app Normal file
View File

@@ -0,0 +1,35 @@
# ./Dockerfile
FROM node:19-alpine
WORKDIR /app
# Copy package.json files for client and api
COPY /client/package*.json /app/client/
COPY /api/package*.json /app/api/
COPY /package*.json /app/
# Install dependencies for both client and api
RUN npm ci
# Copy the current directory contents into the container
COPY /client/ /app/client/
COPY /api/ /app/api/
# Set the memory limit for Node.js
ENV NODE_OPTIONS="--max-old-space-size=2048"
# Build artifacts for the client
RUN cd /app/client && npm run build
# Create the necessary directory and copy the client side code to the api directory
RUN mkdir -p /app/api/client && cp -R /app/client/dist /app/api/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
WORKDIR /app/api
CMD ["npm", "start"]

View File

@@ -1,7 +1,7 @@
MIT License
# MIT License
Copyright (c) 2023 Danny Avila
##
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
@@ -12,6 +12,8 @@ furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
##
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -19,3 +21,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
##
## [Go Back to ReadMe](README.md)

352
README.md
View File

@@ -1,279 +1,149 @@
# ChatGPT Clone #
https://user-images.githubusercontent.com/110412045/223754183-8b7f45ce-6517-4bd5-9b39-c624745bf399.mp4
<p align="center">
<a href="https://discord.gg/NGaa9RPCft">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/110412045/228325485-9d3e618f-a980-44fe-89e9-d6d39164680e.png">
<img src="https://user-images.githubusercontent.com/110412045/228325485-9d3e618f-a980-44fe-89e9-d6d39164680e.png" height="128">
</picture>
<h1 align="center">ChatGPT Clone</h1>
</a>
</p>
<p align="center">
<a aria-label="Join the community on Discord" href="https://discord.gg/NGaa9RPCft">
<img alt="" src="https://img.shields.io/badge/Join%20the%20community-blueviolet.svg?style=for-the-badge&logo=DISCORD&labelColor=000000&logoWidth=20">
</a>
<a aria-label="Sponsors" href="#sponsors">
<img alt="" src="https://img.shields.io/badge/SPONSORS-brightgreen.svg?style=for-the-badge&labelColor=000000&logoWidth=20">
</a>
</p>
## All AI Conversations under One Roof. ##
Assistant AIs are the future and OpenAI revolutionized this movement with ChatGPT. While numerous methods exist to integrate them, this app commemorates the original styling of ChatGPT, with the ability to integrate any current/future AI models, while improving upon original client features, such as conversation search and prompt templates (currently WIP).
Assistant AIs are the future and OpenAI revolutionized this movement with ChatGPT. While numerous UIs exist, this app commemorates the original styling of ChatGPT, with the ability to integrate any current/future AI models, while integrating and improving upon original client features, such as conversation/message search and prompt templates (currently WIP). Through this clone, you can avoid ChatGPT Plus in favor of free or pay-per-call APIs. I will soon deploy a demo of this app. Feel free to contribute, clone, or fork. Currently dockerized.
![clone3](https://user-images.githubusercontent.com/110412045/230538752-9b99dc6e-cd02-483a-bff0-6c6e780fa7ae.gif)
This project was started early in Feb '23, anticipating the release of the official ChatGPT API from OpenAI, and now uses it. Through this clone, you can avoid ChatGPT Plus in favor of free or pay-per-call APIs. I will soon deploy a demo of this app. Feel free to contribute, clone, or fork. Currently dockerized.
## Updates
<details open>
<summary><strong>2023-03-12</strong></summary>
Really thankful for all the issues reported and contributions made, the project's features and improvements have accelerated as result. Honorable mention is [wtlyu](https://github.com/wtlyu) for contributing a lot of mindful code, namely hostname configuration and mobile styling. I will upload images on next release for faster docker setup, and starting updating them simultaneously with this repo.
Many improvements across the board, the biggest is being able to start conversations simultaneously (again thanks to [wtlyu](https://github.com/wtlyu) for bringing it to my attention), as you can switch conversations or start a new chat without any response streaming from a prior one, as the backend will still process/save client responses. Just watch out for any rate limiting from OpenAI/Microsoft if this is done excessively.
Adding support for conversation search is next! Thank you [mysticaltech](https://github.com/mysticaltech) for bringing up a method I can use for this.
</details>
<details>
<details>
<summary><strong>2023-03-09</strong></summary>
Released v.0.0.2
Adds Sydney (jailbroken Bing AI) to the model menu. Thank you [DavesDevFails](https://github.com/DavesDevFails) for bringing it to my attention in this [issue](https://github.com/danny-avila/chatgpt-clone/issues/13). Bing/Sydney now correctly cite links, more styling to come. Fix some overlooked bugs, and model menu doesn't close upon deleting a customGpt.
I've re-enabled the ChatGPT browser client (free version) since it might be working for most people, it no longer works for me. Sydney is the best free route anyway.
</details>
<details>
<summary><strong>2023-03-07</strong></summary>
Due to increased interest in the repo, I've dockerized the app as of this update for quick setup! See setup instructions below. I realize this still takes some time with installing docker dependencies, so it's on the roadmap to have a deployed demo. Besides this, I've made major improvements for a lot of the existing features across the board, mainly UI/UX.
Also worth noting, the method to access the Free Version is no longer working, so I've removed it from model selection until further notice.
</details>
<summary><strong>Previous Updates</strong></summary>
<details>
<summary><strong>2023-03-04</strong></summary>
Custom prompt prefixing and labeling is now supported through the official API. This nets some interesting results when you need ChatGPT for specific uses or entertainment. Select 'CustomGPT' in the model menu to configure this, and you can choose to save the configuration or reference it by conversation. Model selection will change by conversation.
</details>
<details>
<summary><strong>2023-03-01</strong></summary>
Official ChatGPT API is out! Removed davinci since the official API is extremely fast and 10x less expensive. Since user labeling and prompt prefixing is officially supported, I will add a View feature so you can set this within chat, which gives the UI an added use case. I've kept the BrowserClient, since it's free to use like the official site.
The Messages UI correctly mirrors code syntax highlighting. The exact replication of the cursor is not 1-to-1 yet, but pretty close. Later on in the project, I'll implement tests for code edge cases and explore the possibility of running code in-browser. Right now, unknown code defaults to javascript, but will detect language as close as possible.
</details>
<details>
<summary><strong>2023-02-21</strong></summary>
BingAI is integrated (although sadly limited by Microsoft with the 5 msg/convo limit, 50 msgs/day). I will need to handle the case when Bing refuses to give more answers on top of the other styling features I have in mind. Official ChatGPT use is back with the new BrowserClient. Brainstorming how to handle the UI when the Ai model changes, since conversations can't be persisted between them (or perhaps build a way to achieve this at some level).
</details>
<details >
<summary><strong>2023-02-15</strong></summary>
Just got access to Bing AI so I'll be focusing on integrating that through waylaidwanderer's 'experimental' BingAIClient.
</details>
<details>
<summary><strong>2023-02-14</strong></summary>
Official ChatGPT use is no longer possible though I recently used it with waylaidwanderer's [reverse proxy method](https://github.com/waylaidwanderer/node-chatgpt-api/blob/main/README.md#using-a-reverse-proxy), and before that, through leaked models he also discovered.
Currently, this project is only functional with the `text-davinci-003` model.
</details>
</details>
# Table of Contents
- [ChatGPT Clone](#chatgpt-clone)
- [All AI Conversations under One Roof.](#all-ai-conversations-under-one-roof)
- [Updates](#updates)
- [Table of Contents](#table-of-contents)
- [Roadmap](#roadmap)
- [Features](#features)
- [Tech Stack](#tech-stack)
- [Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Usage](#usage)
- [Local](#local)
- [Docker](#docker)
- [Access Tokens](#access-tokens)
- [Proxy](#proxy)
- [Updating](#updating)
- [Use Cases](#use-cases)
- [Origin](#origin)
- [Caveats](#caveats)
- [Regarding use of Official ChatGPT API](#regarding-use-of-official-chatgpt-api)
- [Contributing](#contributing)
- [License](#license)
## Roadmap
> **Warning**
> This is a work in progress. I'm building this in public. FYI there is still a lot of tech debt to cleanup. You can follow the progress here or on my [Linkedin](https://www.linkedin.com/in/danny-avila).
Here are my recently completed and planned features:
- [x] Persistent conversation
- [x] Rename, delete conversations
- [x] UI Error handling
- [x] Bing AI integration
- [x] AI model change handling (start new convos within existing, remembers last selected)
- [x] Code block handling (highlighting, markdown, clipboard, language detection)
- [x] Markdown handling
- [x] Customize prompt prefix/label (custom ChatGPT using official API)
- [x] Server convo pagination (limit fetch and load more with 'show more' button)
- [x] Config file for easy startup (docker compose)
- [x] Mobile styling (thanks to [wtlyu](https://github.com/wtlyu))
- [ ] Bing AI Styling (for suggested responses, convo end, etc.) - **In progress**
- [ ] Add warning before clearing convos
- [ ] Build test suite for CI/CD
- [ ] Conversation Search (by title)
- [ ] Resubmit/edit sent messages
- [ ] Semantic Search Option (requires more tokens)
- [ ] Prompt Templates/Search
- [ ] Refactor/clean up code (tech debt)
- [ ] Optional use of local storage for credentials
- [ ] Deploy demo
### Features
# Features
- Response streaming identical to ChatGPT through server-sent events
- UI from original ChatGPT, including Dark mode
- AI model selection (official ChatGPT API, BingAI, ChatGPT Free)
- Create and Save custom ChatGPTs*
- AI model selection (through 3 endpoints: OpenAI API, BingAI, and ChatGPT Browser)
- Create, Save, & Share custom presets for OpenAI and BingAI endpoints - [More info on customization here](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.3.0)
- Edit and Resubmit messages just like the official site (with conversation branching)
- Search all messages/conversations - [More info here](https://github.com/danny-avila/chatgpt-clone/releases/tag/v0.1.0)
- Integrating plugins soon
^* ChatGPT can be 'customized' by setting a system message or prompt prefix and alternate 'role' to the API request
##
# Sponsors
[More info here](https://platform.openai.com/docs/guides/chat/instructing-chat-models). Here's an [example from this app.]()
Sponsored by <a href="https://github.com/DavidDev1334"><b>@DavidDev1334</b></a>, <a href="https://github.com/mjtechguy"><b>@mjtechguy</b></a>, <a href="https://github.com/Pharrcyde"><b>@Pharrcyde</b></a>, & <a href="https://github.com/fuegovic"><b>@fuegovic</b></a>
### Tech Stack
- Utilizes [node-chatgpt-api](https://github.com/waylaidwanderer/node-chatgpt-api)
- No React boilerplate/toolchain/clone tutorials, created from scratch with react@latest
- Use of Tailwind CSS and [shadcn/ui](https://github.com/shadcn/ui) components
- Docker, useSWR, Redux, Express, MongoDB, [Keyv](https://www.npmjs.com/package/keyv)
## Getting Started
### Prerequisites
- npm
- Node.js >= 19.0.0
- MongoDB installed or [MongoDB Atlas](https://account.mongodb.com/account/login) (required if not using Docker)
- [Docker (optional)](https://www.docker.com/get-started/)
- [OpenAI API key](https://platform.openai.com/account/api-keys)
- BingAI, ChatGPT access tokens (optional, free AIs)
## Usage
- **Clone/download** the repo down where desired
```bash
git clone https://github.com/danny-avila/chatgpt-clone.git
```
- If using MongoDB Atlas, remove `&w=majority` from default connection string.
### Local
- **Run npm** install in both the api and client directories
- **Provide** all credentials, (API keys, access tokens, and Mongo Connection String) in api/.env [(see .env example)](api/.env.example)
- **Run** `npm run build` in /client/ dir, `npm start` in /api/ dir
- **Visit** http://localhost:3080 (default port) & enjoy
By default, only local machine can access this server. To share within network or serve as a public server, set `HOST` to `0.0.0.0` in `.env` file
### Docker
- **Provide** all credentials, (API keys, access tokens, and Mongo Connection String) in [docker-compose.yml](docker-compose.yml) under api service
- **Build images** in both /api/ and /client/ directories (will eventually share through docker hub)
- `api/`
```bash
docker build -t node-api .
```
- `client/`
```bash
docker build -t react-client .
```
- **Run** `docker-compose build` in project root dir and then `docker-compose up` to start the app
### Access Tokens
##
## **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)
<details>
<summary><strong>ChatGPT Free Instructions</strong></summary>
<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/.
To get your Access token For ChatGPT 'Free Version', login to chat.openai.com, then visit https://chat.openai.com/api/auth/session.
**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
**Warning:** There may be a high chance of your account being banned with this method. Continue doing so at your own risk.
**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)
</details>
<details>
<summary><strong>BingAI Instructions</strong></summary>
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.
<summary><strong>General Information</strong></summary>
**Note:** Specific error handling and styling for this model is still in progress.
</details>
### Proxy
If your server cannot connect to the chatGPT API server by some reason, (eg in China). You can set a environment variable `PROXY`. This will be transmitted to `node-chatgpt-api` interface.
**Warning:** `PROXY` is not `reverseProxyUrl` in `node-chatgpt-api`
<details>
<summary><strong>Set up proxy in local environment </strong></summary>
Here is two ways to set proxy.
- Option 1: system level environment
`export PROXY="http://127.0.0.1:7890"`
- Option 2: set in .env file
`PROXY="http://127.0.0.1:7890"`
**Change `http://127.0.0.1:7890` to your proxy server**
* [Project Origin](documents/general_info/project_origin.md)
* [Multilingual Information](documents/general_info/multilingual_information.md)
* [Roadmap](documents/general_info/roadmap.md)
* [Tech Stack](documents/general_info/tech_stack.md)
* [Changelog](CHANGELOG.md)
* [Bing Jailbreak Info](documents/general_info/bing_jailbreak_info.md)
</details>
<details>
<summary><strong>Set up proxy in docker environment </strong></summary>
set in docker-compose.yml file, under services - api - environment
```
api:
...
environment:
...
- "PROXY=http://127.0.0.1:7890"
# add this line ↑
```
**Change `http://127.0.0.1:7890` to your proxy server**
<summary><strong>Features</strong></summary>
* [User Auth System](documents/features/user_auth_system.md)
* [Proxy](documents/features/proxy.md)
</details>
### Updating
- As the project is still a work-in-progress, you should pull the latest and run the steps over. Reset your browser cache/clear site data.
<details>
<summary><strong>Cloud Deployment</strong></summary>
## Use Cases ##
* [Heroku](documents/deployment/heroku.md)
</details>
- One stop shop for all conversational AIs, with the added bonus of searching past conversations.
- Using the official API, you'd have to generate 7.5 million words to expense the same cost as ChatGPT Plus ($20).
- ChatGPT/Google Bard/Bing AI conversations are lost in space or
cannot be searched past a certain timeframe.
- **Customize ChatGPT**
<details>
<summary><strong>Contributions</strong></summary>
![use case example](./images/use_case3.png "Make a Custom GPT")
* [Code of Conduct](documents/contributions/code_of_conduct.md)
* [Contributor Guidelines](documents/contributions/contributor_guidelines.md)
* [Documentation Guidelines](documents/contributions/documentation_guidelines.md)
* [Code Standards and Conventions](documents/contributions/coding_conventions.md)
* [Testing](documents/contributions/testing.md)
* [Pull Request Template](documents/contributions/pull_request_template.md)
* [Contributors](CONTRIBUTORS.md)
* [Trello Board](https://trello.com/b/17z094kq/chatgpt-clone)
</details>
- **API is not as limited as ChatGPT Free (at [chat.openai.com](https://chat.openai.com/chat))**
<details>
<summary><strong>Report Templates</strong></summary>
![use case example](./images/use_case2.png "chat.openai.com is getting more limited by the day!")
* [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>
- **ChatGPT Free is down.**
##
### [Alternative Documentation](https://chatgpt-clone.gitbook.io/chatgpt-clone-docs/get-started/docker)
![use case example](./images/use_case.png "GPT is down! Plus is too expensive!")
##
## Star History
## Origin ##
This project was originally created as a Minimum Viable Product (or MVP) for the [@HackReactor](https://github.com/hackreactor/) Bootcamp. It was built with OpenAI response streaming and most of the UI completed in under 20 hours. During the end of that time, I had most of the UI and basic functionality done. This was created without using any boilerplates or templates, including create-react-app and other toolchains. I didn't follow any 'un-official chatgpt' video tutorials, and simply referenced the official site for the UI. The purpose of the exercise was to learn setting up a full stack project from scratch. Please feel free to give feedback, suggestions, or fork the project for your own use.
[![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!
## Caveats
### Regarding use of Official ChatGPT API
From [@waylaidwanderer](https://github.com/waylaidwanderer/node-chatgpt-api/blob/main/README.md#caveats):
For new features, components, or extensions, please open an issue and discuss before sending a PR.
Since `gpt-3.5-turbo` is ChatGPT's underlying model, I had to do my best to replicate the way the official ChatGPT website uses it.
This means my implementation or the underlying model may not behave exactly the same in some ways:
- Conversations are not tied to any user IDs, so if that's important to you, you should implement your own user ID system.
- ChatGPT's model parameters (temperature, frequency penalty, etc.) are unknown, so I set some defaults that I thought would be reasonable.
- Conversations are limited to roughly the last 3000 tokens, so earlier messages may be forgotten during longer conversations.
- This works in a similar way to ChatGPT, except I'm pretty sure they have some additional way of retrieving context from earlier messages when needed (which can probably be achieved with embeddings, but I consider that out-of-scope for now).
- Join the [Discord community](https://discord.gg/NGaa9RPCft)
## Contributing
If you'd like to contribute, please create a pull request with a detailed description of your changes.
## License
This project is licensed under the MIT License.
This project exists in its current state thanks to all the people who contribute
---
<a href="https://github.com/danny-avila/chatgpt-clone/graphs/contributors">
<img src="https://contrib.rocks/image?repo=danny-avila/chatgpt-clone" />
</a>

47
SECURITY.md Normal file
View File

@@ -0,0 +1,47 @@
# Security Policy
## Reporting a Vulnerability
We take security seriously and appreciate the efforts of security researchers to improve the security of our codebase.
If you discover a security vulnerability within our project, please follow these guidelines to report it to us:
**Note: Only report sensible vulnerability report details via Github Security Advisory System. Every other communication channel are public and should be used only to initiate first contact and to initiate a private communication channel.**
### Communication channels
- **Option 1: GitHub Security Advisory System**: We encourage you to use GitHub's Security Advisory system to report any security vulnerabilities you find. This allows us to receive vulnerability reports directly through GitHub. You can find more information on how to submit a security advisory report in the [GitHub Security Advisories documentation](https://docs.github.com/en/code-security/getting-started-with-security-vulnerability-alerts/about-github-security-advisories).
- **Option 2: Github issues**: You can initiate first contact via Github Issues. **Please note that initial contact through Discord should not include any sensitive details.**
- **Option 3: Discord Server**: You can join our [Discord community](https://discord.gg/NGaa9RPCft) and initiate first contact in the `#issues` channel. **Please note that initial contact through Discord should not include any sensitive details.**
_After initial contact, we will use this initial contact to establish a private communication channel for further discussion._
### When submitting a vulnerability report, please provide us with the following information:
- A clear description of the vulnerability, including steps to reproduce it
- The version(s) of the project affected by the vulnerability
- Any additional information that may be useful for understanding and addressing the issue
We will make every effort to acknowledge your report within 72 hours and keep you informed of its progress towards resolution.
## Security Updates and Patching
We are committed to maintaining the security of our open-source project named ChatGPT-Clone and promptly addressing any identified vulnerabilities. To ensure the security of our project, we follow these practices:
- We prioritize security updates for the current major release of our software.
- We actively monitor the GitHub Security Advisory system and the `#security-reports` channel on Discord for any vulnerability reports.
- We promptly review and validate reported vulnerabilities and take appropriate actions to address them.
- We release security patches and updates in a timely manner to mitigate any identified vulnerabilities.
Please note that as a security-conscious community, we may not always disclose detailed information about security issues until we have determined that doing so would not put our users or the project at risk. We appreciate your understanding and cooperation in these matters.
## Scope
This security policy applies to the following GitHub repository:
- Repository: [ChatGPT-Clone](https://github.com/danny-avila/chatgpt-clone)
## Contact
If you have any questions or concerns regarding the security of our project, please join our [Discord community](https://discord.gg/NGaa9RPCft) and report them in the appropriate channel.
You can also reach out to us by [opening an issue](https://github.com/danny-avila/chatgpt-clone/issues/new) on GitHub.
Please note that the response time may vary depending on the nature and severity of the inquiry.
## Acknowledgments
We would like to express our gratitude to the security researchers and community members who help us improve the security of our project. Your contributions are invaluable, and we sincerely appreciate your efforts.
## Bug Bounty Program
We do not currently have a bug bounty program in place. However, we welcome and appreciate any security-related contributions through pull requests (PRs) that address vulnerabilities in our codebase.
We believe in the power of collaboration to improve the security of our project and invite you to join us in making it more robust.

View File

@@ -1,2 +0,0 @@
/node_modules
.env

View File

@@ -1,22 +1,177 @@
# Server configuration.
# The server will listen to localhost:3080 request by default. You can set the target ip as you want.
# If you want this server can be used outside your local machine, for example to share with other
# machine or expose this from a docker container, set HOST=0.0.0.0 or your external ip interface.
#
# Tips: HOST=0.0.0.0 means listening on all interface. It's not a real ip. Use localhost:port rather
# than 0.0.0.0:port to open it.
HOST=localhost
##########################
# Server configuration:
##########################
# The server will listen to localhost:3080 by default. You can change the target IP as you want.
# If you want to make this server available externally, for example to share the server with others
# or expose this from a Docker container, set host to 0.0.0.0 or your external IP interface.
# Tips: Setting host to 0.0.0.0 means listening on all interfaces. It's not a real IP.
# Use localhost:port rather than 0.0.0.0:port to access the server.
# Set Node env to development if running in dev mode.
HOST=localhost
PORT=3080
NODE_ENV=development
NODE_ENV=production
# Change this to proxy any API request. It's useful if your machine have difficulty calling the original API server.
# PROXY="http://YOUR_PROXY_SERVER"
# Change this to proxy any API request.
# It's useful if your machine has difficulty calling the original API server.
# PROXY=
# Change this to your MongoDB URI if different and I recommend appending chatgpt-clone
MONGO_URI="mongodb://127.0.0.1:27017/chatgpt-clone"
# Change this to your MongoDB URI if different. I recommend appending chatgpt-clone.
MONGO_URI=mongodb://127.0.0.1:27017/chatgpt-clone
# API key configuration.
# Leave blank if you don't want them.
##########################
# OpenAI Endpoint:
##########################
# Access key from OpenAI platform.
# 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=
CHATGPT_TOKEN=
BING_TOKEN=
# Identify the available models, separated by commas *without spaces*.
# The first will be default.
# Leave it blank to use internal settings.
OPENAI_MODELS=gpt-3.5-turbo,gpt-3.5-turbo-0301,text-davinci-003,gpt-4
# Reverse proxy settings for OpenAI:
# https://github.com/waylaidwanderer/node-chatgpt-api#using-a-reverse-proxy
# OPENAI_REVERSE_PROXY=
##########################
# AZURE Endpoint:
##########################
# To use Azure with this project, set the following variables. These will be used to build the API URL.
# Chat completion:
# `https://{AZURE_OPENAI_API_INSTANCE_NAME}.openai.azure.com/openai/deployments/{AZURE_OPENAI_API_DEPLOYMENT_NAME}/chat/completions?api-version={AZURE_OPENAI_API_VERSION}`;
# You should also consider changing the `OPENAI_MODELS` variable above to the models available in your instance/deployment.
# Note: I've noticed that the Azure API is much faster than the OpenAI API, so the streaming looks almost instantaneous.
# AZURE_OPENAI_API_KEY=
# AZURE_OPENAI_API_INSTANCE_NAME=
# AZURE_OPENAI_API_DEPLOYMENT_NAME=
# AZURE_OPENAI_API_VERSION=
# AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME= # Optional, but may be used in future updates
# AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME= # Optional, but may be used in future updates
##########################
# BingAI Endpoint:
##########################
# Also used for Sydney and jailbreak
# BingAI Tokens: the "_U" cookies value from bing.com
# 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"
# BingAI Host:
# Necessary for some people in different countries, e.g. China (https://cn.bing.com)
# Leave it blank to use default server.
# BINGAI_HOST=https://cn.bing.com
##########################
# ChatGPT Endpoint:
##########################
# ChatGPT Browser Client (free but use at your own risk)
# Access token from https://chat.openai.com/api/auth/session
# Exposes your access token to `CHATGPT_REVERSE_PROXY`
# Set to "user_provided" to allow the user to provide its token from the UI.
# Leave it blank to disable this endpoint
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,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:
##########################
# ENABLING SEARCH MESSAGES/CONVOS
# Requires the installation of the free self-hosted Meilisearch or a paid Remote Plan (Remote not tested)
# The easiest setup for this is through docker-compose, which takes care of it for you.
SEARCH=false
# REQUIRED FOR SEARCH: MeiliSearch Host, mainly for the API server to connect to the search server.
# Replace '0.0.0.0' with 'meilisearch' if serving MeiliSearch with docker-compose.
MEILI_HOST=http://0.0.0.0:7700
# REQUIRED FOR SEARCH: MeiliSearch HTTP Address, mainly for docker-compose to expose the search server.
# Replace '0.0.0.0' with 'meilisearch' if serving MeiliSearch with docker-compose.
MEILI_HTTP_ADDR=0.0.0.0:7700
# REQUIRED FOR SEARCH: In production env., a secure key is needed. You can generate your own.
# This master key must be at least 16 bytes, composed of valid UTF-8 characters.
# MeiliSearch will throw an error and refuse to launch if no master key is provided,
# or if it is under 16 bytes. MeiliSearch will suggest a secure autogenerated master key.
# Using docker, it seems recognized as production so use a secure key.
# This is a ready made secure key for docker-compose, you can replace it with your own.
MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt
##########################
# User System:
##########################
# Google:
# Add your Google Client ID and Secret here, you must register an app with Google Cloud to get these values
# https://cloud.google.com/
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CALLBACK_URL=/oauth/google/callback
#JWT:
JWT_SECRET_DEV=secret
# Add a secure secret for production if deploying to live domain.
JWT_SECRET_PROD=secret
# Set the expiration delay for the secure cookie with the JWT token
# Delay is in millisecond e.g. 7 days is 1000*60*60*24*7
SESSION_EXPIRY=1000 * 60 * 60 * 24 * 7
# Site URLs:
# Don't forget to set Node env to development in the Server configuration section above
# if you want to run in dev mode
CLIENT_URL_DEV=http://localhost:3090
SERVER_URL_DEV=http://localhost:3080
# Change these values to domain if deploying:
CLIENT_URL_PROD=http://localhost:3080
SERVER_URL_PROD=http://localhost:3080

View File

@@ -1,16 +0,0 @@
FROM node:19-alpine
WORKDIR /api
# copy package.json into the container at /api
COPY package*.json /api/
# install dependencies
RUN npm install
# Copy the current directory contents into the container at /api
COPY . /api/
# 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"]
# docker build -t node-api .

View File

@@ -1,56 +0,0 @@
require('dotenv').config();
const { KeyvFile } = require('keyv-file');
const askBing = async ({ text, progressCallback, convo }) => {
const { BingAIClient } = (await import('@waylaidwanderer/chatgpt-api'));
const bingAIClient = new BingAIClient({
// "_U" cookie from bing.com
userToken: process.env.BING_TOKEN,
// If the above doesn't work, provide all your cookies as a string instead
// cookies: '',
debug: false,
cache: { store: new KeyvFile({ filename: './data/cache.json' }) },
proxy: process.env.PROXY || null,
});
let options = {
onProgress: async (partialRes) => await progressCallback(partialRes),
};
if (convo) {
options = { ...options, ...convo };
}
const res = await bingAIClient.sendMessage(text, options
);
return res;
// Example response for reference
// {
// conversationSignature: 'wwZ2GC/qRgEqP3VSNIhbPGwtno5RcuBhzZFASOM+Sxg=',
// conversationId: '51D|BingProd|026D3A4017554DE6C446798144B6337F4D47D5B76E62A31F31D0B1D0A95ED868',
// clientId: '914800201536527',
// invocationId: 1,
// conversationExpiryTime: '2023-02-15T21:48:46.2892088Z',
// response: 'Hello, this is Bing. Nice to meet you. 😊',
// details: {
// text: 'Hello, this is Bing. Nice to meet you. 😊',
// author: 'bot',
// createdAt: '2023-02-15T15:48:43.0631898+00:00',
// timestamp: '2023-02-15T15:48:43.0631898+00:00',
// messageId: '9d0c9a80-91b1-49ab-b9b1-b457dc3fe247',
// requestId: '5b252ef8-4f09-4c08-b6f5-4499d2e12fba',
// offense: 'None',
// adaptiveCards: [ [Object] ],
// sourceAttributions: [],
// feedback: { tag: null, updatedOn: null, type: 'None' },
// contentOrigin: 'DeepLeo',
// privacy: null,
// suggestedResponses: [ [Object], [Object], [Object] ]
// }
// }
};
module.exports = { askBing };

View File

@@ -1,34 +0,0 @@
require('dotenv').config();
const { KeyvFile } = require('keyv-file');
const clientOptions = {
// Warning: This will expose your access token to a third party. Consider the risks before using this.
reverseProxyUrl: 'https://bypass.duti.tech/api/conversation',
// Access token from https://chat.openai.com/api/auth/session
accessToken: process.env.CHATGPT_TOKEN,
// debug: true
proxy: process.env.PROXY || null,
};
const browserClient = async ({ text, progressCallback, convo }) => {
const { ChatGPTBrowserClient } = await import('@waylaidwanderer/chatgpt-api');
const store = {
store: new KeyvFile({ filename: './data/cache.json' })
};
const client = new ChatGPTBrowserClient(clientOptions, store);
let options = {
onProgress: async (partialRes) => await progressCallback(partialRes)
};
if (!!convo.parentMessageId && !!convo.conversationId) {
options = { ...options, ...convo };
}
const res = await client.sendMessage(text, options);
return res;
};
module.exports = { browserClient };

View File

@@ -1,32 +0,0 @@
require('dotenv').config();
const { KeyvFile } = require('keyv-file');
const clientOptions = {
modelOptions: {
model: 'gpt-3.5-turbo'
},
proxy: process.env.PROXY || null,
debug: false
};
const askClient = async ({ text, progressCallback, convo }) => {
const ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default;
const store = {
store: new KeyvFile({ filename: './data/cache.json' })
};
const client = new ChatGPTClient(process.env.OPENAI_KEY, clientOptions, store);
let options = {
onProgress: async (partialRes) => await progressCallback(partialRes)
};
if (!!convo.parentMessageId && !!convo.conversationId) {
options = { ...options, ...convo };
}
const res = await client.sendMessage(text, options);
return res;
};
module.exports = { askClient };

View File

@@ -1,38 +0,0 @@
require('dotenv').config();
const { KeyvFile } = require('keyv-file');
const clientOptions = {
modelOptions: {
model: 'gpt-3.5-turbo'
},
proxy: process.env.PROXY || null,
debug: false
};
const customClient = async ({ text, progressCallback, convo, promptPrefix, chatGptLabel }) => {
const ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default;
const store = {
store: new KeyvFile({ filename: './data/cache.json' })
};
clientOptions.chatGptLabel = chatGptLabel;
if (promptPrefix?.length > 0) {
clientOptions.promptPrefix = promptPrefix;
}
const client = new ChatGPTClient(process.env.OPENAI_KEY, clientOptions, store);
let options = {
onProgress: async (partialRes) => await progressCallback(partialRes)
};
if (!!convo.parentMessageId && !!convo.conversationId) {
options = { ...options, ...convo };
}
const res = await client.sendMessage(text, options);
return res;
};
module.exports = customClient;

View File

@@ -1,38 +0,0 @@
require('dotenv').config();
const Keyv = require('keyv');
const { Configuration, OpenAIApi } = require('openai');
const messageStore = new Keyv(process.env.MONGODB_URI, { namespace: 'chatgpt' });
const ask = async (question, progressCallback, convo) => {
const { ChatGPTAPI } = await import('chatgpt');
const api = new ChatGPTAPI({ apiKey: process.env.OPENAI_KEY, messageStore });
let options = {
onProgress: async (partialRes) => {
if (partialRes.text.length > 0) {
await progressCallback(partialRes);
}
}
};
if (!!convo.parentMessageId && !!convo.conversationId) {
options = { ...options, ...convo };
}
const res = await api.sendMessage(question, options);
return res;
};
const titleConvo = async (message, response, model) => {
const configuration = new Configuration({
apiKey: process.env.OPENAI_KEY
});
const openai = new OpenAIApi(configuration);
const completion = await openai.createCompletion({
model: 'text-davinci-002',
prompt: `Write a short title in title case, ideally in 5 words or less, and do not refer to the user or ${model}, that summarizes this conversation:\nUser:"${message}"\n${model}:"${response}"\nTitle: `
});
return completion.data.choices[0].text.replace(/\n/g, '');
};
module.exports = { ask, titleConvo };

80
api/app/clients/bingai.js Normal file
View File

@@ -0,0 +1,80 @@
require('dotenv').config();
const { KeyvFile } = require('keyv-file');
const askBing = async ({
text,
parentMessageId,
conversationId,
jailbreak,
jailbreakConversationId,
context,
systemMessage,
conversationSignature,
clientId,
invocationId,
toneStyle,
token,
onProgress
}) => {
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,
// If the above doesn't work, provide all your cookies as a string instead
// cookies: '',
debug: false,
cache: store,
host: process.env.BINGAI_HOST || null,
proxy: process.env.PROXY || null
});
let options = {};
if (jailbreakConversationId == 'false') {
jailbreakConversationId = false;
}
if (jailbreak)
options = {
jailbreakConversationId: jailbreakConversationId || jailbreak,
context,
systemMessage,
parentMessageId,
toneStyle,
onProgress
};
else {
options = {
conversationId,
context,
systemMessage,
parentMessageId,
toneStyle,
onProgress
};
// don't give those parameters for new conversation
// for new conversation, conversationSignature always is null
if (conversationSignature) {
options.conversationSignature = conversationSignature;
options.clientId = clientId;
options.invocationId = invocationId;
}
}
console.log('bing options', options);
const res = await bingAIClient.sendMessage(text, options);
return res;
// for reference:
// https://github.com/waylaidwanderer/node-chatgpt-api/blob/main/demos/use-bing-client.js
};
module.exports = { askBing };

View File

@@ -0,0 +1,49 @@
require('dotenv').config();
const { KeyvFile } = require('keyv-file');
const browserClient = async ({
text,
parentMessageId,
conversationId,
model,
token,
onProgress,
abortController,
userId
}) => {
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',
// Access token from https://chat.openai.com/api/auth/session
accessToken:
process.env.CHATGPT_TOKEN == 'user_provided' ? token : process.env.CHATGPT_TOKEN ?? null,
model: model,
debug: false,
proxy: process.env.PROXY || null,
user: userId
};
const client = new ChatGPTBrowserClient(clientOptions, store);
let options = { onProgress, abortController };
if (!!parentMessageId && !!conversationId) {
options = { ...options, parentMessageId, conversationId };
}
console.log('gptBrowser clientOptions', clientOptions);
if (parentMessageId === '00000000-0000-0000-0000-000000000000') {
delete options.conversationId;
}
const res = await client.sendMessage(text, options);
return res;
};
module.exports = { browserClient };

View File

@@ -0,0 +1,92 @@
require('dotenv').config();
const { KeyvFile } = require('keyv-file');
const { genAzureEndpoint } = require('../../utils/genAzureEndpoints');
const tiktoken = require('@dqbd/tiktoken');
const encoding_for_model = tiktoken.encoding_for_model;
const askClient = async ({
text,
parentMessageId,
conversationId,
model,
oaiApiKey,
chatGptLabel,
promptPrefix,
temperature,
top_p,
presence_penalty,
frequency_penalty,
onProgress,
abortController,
userId
}) => {
const { ChatGPTClient } = await import('@waylaidwanderer/chatgpt-api');
const store = {
store: new KeyvFile({ filename: './data/cache.json' })
};
const azure = process.env.AZURE_OPENAI_API_KEY ? true : false;
if (promptPrefix == null) {
promptText = 'You are ChatGPT, a large language model trained by OpenAI.';
} else {
promptText = promptPrefix;
}
const maxContextTokens = model === 'gpt-4' ? 8191 : model === 'gpt-4-32k' ? 32767 : 4095; // 1 less than maximum
const clientOptions = {
reverseProxyUrl: process.env.OPENAI_REVERSE_PROXY || null,
azure,
maxContextTokens,
modelOptions: {
model,
temperature,
top_p,
presence_penalty,
frequency_penalty
},
chatGptLabel,
promptPrefix,
proxy: process.env.PROXY || null
// debug: true
};
let apiKey = oaiApiKey ? oaiApiKey : process.env.OPENAI_KEY || null;
if (azure) {
apiKey = oaiApiKey ? oaiApiKey : process.env.AZURE_OPENAI_API_KEY || null;
clientOptions.reverseProxyUrl = genAzureEndpoint({
azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME,
azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME,
azureOpenAIApiVersion: process.env.AZURE_OPENAI_API_VERSION
});
}
const client = new ChatGPTClient(apiKey, clientOptions, store);
const options = {
onProgress,
abortController,
...(parentMessageId && conversationId ? { parentMessageId, conversationId } : {})
};
const enc = encoding_for_model(model);
const text_tokens = enc.encode(text);
const prompt_tokens = enc.encode(promptText);
// console.log("Prompt tokens = ", prompt_tokens.length);
// console.log("Message Tokens = ", text_tokens.length);
const res = await client.sendMessage(text, { ...options, userId });
// return res;
// create a new response object that includes the token counts
const newRes = {
...res,
usage: {
prompt_tokens: prompt_tokens.length,
completion_tokens: text_tokens.length,
total_tokens: prompt_tokens.length + text_tokens.length
}
};
return newRes;
};
module.exports = { askClient };

View File

@@ -1,52 +0,0 @@
const { ModelOperations } = require('@vscode/vscode-languagedetection');
const languages = require('../utils/languages.js');
const codeRegex = /(```[\s\S]*?```)/g;
// const languageMatch = /```(\w+)/;
const replaceRegex = /```\w+\n/g;
const detectCode = async (input) => {
try {
let text = input;
if (!text.match(codeRegex)) {
return text;
}
const langMatches = text.match(replaceRegex);
if (langMatches?.length > 0) {
langMatches.forEach(match => {
let lang = match.split('```')[1].trim();
if (languages.has(lang)) {
return;
}
console.log('[detectCode.js] replacing', match, 'with', '```shell');
text = text.replace(match, '```shell\n');
});
return text;
}
const modelOperations = new ModelOperations();
const regexSplit = (await import('./regexSplit.mjs')).default;
const parts = regexSplit(text, codeRegex);
const output = parts.map(async (part) => {
if (part.match(codeRegex)) {
const code = part.slice(3, -3);
let lang = (await modelOperations.runModel(code))[0].languageId;
return part.replace(/^```/, `\`\`\`${languages.has(lang) ? lang : 'shell'}`);
} else {
return part;
}
});
return (await Promise.all(output)).join('');
} catch (e) {
console.log('Error in detectCode function\n', e);
return input;
}
};
module.exports = detectCode;

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

View File

@@ -1,21 +1,15 @@
const { askClient } = require('./chatgpt-client');
const { browserClient } = require('./chatgpt-browser');
const customClient = require('./chatgpt-custom');
const { askBing } = require('./bingai');
const { askSydney } = require('./sydney');
const { askClient } = require('./clients/chatgpt-client');
const { browserClient } = require('./clients/chatgpt-browser');
const { askBing } = require('./clients/bingai');
const titleConvo = require('./titleConvo');
const getCitations = require('./getCitations');
const citeText = require('./citeText');
const detectCode = require('./detectCode');
const getCitations = require('../lib/parse/getCitations');
const citeText = require('../lib/parse/citeText');
module.exports = {
askClient,
browserClient,
customClient,
askBing,
askSydney,
titleConvo,
getCitations,
citeText,
detectCode
};
citeText
};

View File

@@ -1,46 +0,0 @@
const primaryRegex = /```([^`\n]*?)\n([\s\S]*?)\n```/g;
const secondaryRegex = /```([^`\n]*?)\n?([\s\S]*?)\n?```/g;
const unenclosedCodeTest = (text) => {
let workingText = text;
// if (workingText.startsWith('<') || (!workingText.startsWith('`') && workingText.match(/```/g)?.length === 1)) {
// workingText = `\`\`\`${workingText}`
// }
return workingText.trim();
};
export default function regexSplit(string) {
let matches = [...string.matchAll(primaryRegex)];
if (!matches[0]) {
matches = [...string.matchAll(secondaryRegex)];
}
const output = [matches[0].input.slice(0, matches[0].index)];
// console.log(matches);
for (let i = 0; i < matches.length; i++) {
const [fullMatch, language, code] = matches[i];
// const formattedCode = code.replace(/`+/g, '\\`');
output.push(`\`\`\`${language}\n${code}\n\`\`\``);
if (i < matches.length - 1) {
let nextText = string.slice(matches[i].index + fullMatch.length, matches[i + 1].index);
nextText = unenclosedCodeTest(nextText);
output.push(nextText);
} else {
const lastMatch = matches[matches.length - 1][0];
// console.log(lastMatch);
// console.log(matches[0].input.split(lastMatch));
let rest = matches[0].input.split(lastMatch)[1]
if (rest) {
rest = unenclosedCodeTest(rest);
output.push(rest);
}
}
}
return output;
}

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,36 +0,0 @@
require('dotenv').config();
const { KeyvFile } = require('keyv-file');
const askSydney = async ({ text, progressCallback, convo }) => {
const { BingAIClient } = (await import('@waylaidwanderer/chatgpt-api'));
const sydneyClient = new BingAIClient({
// "_U" cookie from bing.com
userToken: process.env.BING_TOKEN,
// If the above doesn't work, provide all your cookies as a string instead
// cookies: '',
debug: false,
cache: { store: new KeyvFile({ filename: './data/cache.json' }) }
});
let options = {
jailbreakConversationId: true,
onProgress: async (partialRes) => await progressCallback(partialRes),
};
if (convo.parentMessageId) {
options = { ...options, jailbreakConversationId: convo.jailbreakConversationId, parentMessageId: convo.parentMessageId };
}
console.log('sydney options', options);
const res = await sydneyClient.sendMessage(text, options
);
return res;
// for reference:
// https://github.com/waylaidwanderer/node-chatgpt-api/blob/main/demos/use-bing-client.js
};
module.exports = { askSydney };

View File

@@ -1,24 +1,78 @@
const { Configuration, OpenAIApi } = require('openai');
const _ = require('lodash');
const { genAzureEndpoint } = require('../utils/genAzureEndpoints');
const titleConvo = async ({ message, response, model }) => {
const configuration = new Configuration({
apiKey: process.env.OPENAI_KEY
});
const openai = new OpenAIApi(configuration);
const completion = await openai.createChatCompletion({
model: 'gpt-3.5-turbo',
messages: [
{
role: 'system',
content:
'You are a title-generator with one job: giving a conversation, detect the language and titling the conversation provided by a user in title case, using the same language.'
},
{ role: 'user', content: `In 5 words or less, summarize the conversation below with a title in title case using the language the user writes in. Don't refer to the participants of the conversation by name. Do not include punctuation or quotation marks. Your response should be in title case, exclusively containing the title. Conversation:\n\nUser: "${message}"\n\n${model}: "${response}"\n\nTitle: ` },
]
});
const proxyEnvToAxiosProxy = (proxyString) => {
if (!proxyString) return null;
//eslint-disable-next-line
return completion.data.choices[0].message.content.replace(/["\.]/g, '');
const regex = /^([^:]+):\/\/(?:([^:@]*):?([^:@]*)@)?([^:]+)(?::(\d+))?/;
const [, protocol, username, password, host, port] = proxyString.match(regex);
const proxyConfig = {
protocol,
host,
port: port ? parseInt(port) : undefined,
auth: username && password ? { username, password } : undefined
};
return proxyConfig;
};
module.exports = titleConvo;
const titleConvo = async ({ endpoint, text, response, oaiApiKey }) => {
let title = 'New Chat';
const ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default;
try {
const instructionsPayload = {
role: 'system',
content: `Detect user language and write in the same language an extremely concise title for this conversation, which you must accurately detect. Write in the detected language. Title in 5 Words or Less. No Punctuation or Quotation. All first letters of every word should be capitalized and complete only the title in User Language only.
||>User:
"${text}"
||>Response:
"${JSON.stringify(response?.text)}"
||>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
};
const titleGenClientOptions = JSON.parse(JSON.stringify(options));
titleGenClientOptions.modelOptions = {
model: 'gpt-3.5-turbo',
temperature: 0,
presence_penalty: 0,
frequency_penalty: 0
};
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, ' ').replaceAll('"', '').trim();
} catch (e) {
console.error(e);
console.log('There was an issue generating title, see error above');
}
console.log('CONVERSATION TITLE', title);
return title;
};
const throttledTitleConvo = _.throttle(titleConvo, 1000);
module.exports = throttledTitleConvo;

View File

@@ -17,7 +17,7 @@ if (!cached) {
cached = global.mongoose = { conn: null, promise: null };
}
async function dbConnect() {
async function connectDb() {
if (cached.conn) {
return cached.conn;
}
@@ -41,4 +41,4 @@ async function dbConnect() {
return cached.conn;
}
module.exports = dbConnect;
module.exports = connectDb;

70
api/lib/db/indexSync.js Normal file
View File

@@ -0,0 +1,70 @@
const mongoose = require('mongoose');
const Conversation = mongoose.models.Conversation;
const Message = mongoose.models.Message;
const { MeiliSearch } = require('meilisearch');
let currentTimeout = null;
// eslint-disable-next-line no-unused-vars
async function indexSync(req, res, next) {
try {
if (!process.env.MEILI_HOST || !process.env.MEILI_MASTER_KEY || !process.env.SEARCH) {
throw new Error('Meilisearch not configured, search will be disabled.');
}
const client = new MeiliSearch({
host: process.env.MEILI_HOST,
apiKey: process.env.MEILI_MASTER_KEY
});
const { status } = await client.health();
// console.log(`Meilisearch: ${status}`);
const result = status === 'available' && !!process.env.SEARCH;
if (!result) {
throw new Error('Meilisearch not available');
}
const messageCount = await Message.countDocuments();
const convoCount = await Conversation.countDocuments();
const messages = await client.index('messages').getStats();
const convos = await client.index('convos').getStats();
const messagesIndexed = messages.numberOfDocuments;
const convosIndexed = convos.numberOfDocuments;
console.log(`There are ${messageCount} messages in the database, ${messagesIndexed} indexed`);
console.log(`There are ${convoCount} convos in the database, ${convosIndexed} indexed`);
if (messageCount !== messagesIndexed) {
console.log('Messages out of sync, indexing');
await Message.syncWithMeili();
}
if (convoCount !== convosIndexed) {
console.log('Convos out of sync, indexing');
await Conversation.syncWithMeili();
}
} catch (err) {
// console.log('in index sync');
if (err.message.includes('not found')) {
console.log('Creating indices...');
currentTimeout = setTimeout(async () => {
try {
await Message.syncWithMeili();
await Conversation.syncWithMeili();
} catch (err) {
console.error('Trouble creating indices, try restarting the server.');
}
}, 750);
} else {
console.error(err);
// res.status(500).json({ error: 'Server error' });
}
}
}
process.on('exit', () => {
console.log('Clearing sync timeouts before exiting...');
clearTimeout(currentTimeout);
});
module.exports = indexSync;

118
api/lib/db/migrateDb.js Normal file
View File

@@ -0,0 +1,118 @@
const mongoose = require('mongoose');
const { Conversation } = require('../../models/Conversation');
const { getMessages } = require('../../models/');
const migrateToStrictFollowParentMessageIdChain = async () => {
try {
const conversations = await Conversation.find({ endpoint: null, model: null }).exec();
if (!conversations || conversations.length === 0) return { noNeed: true };
console.log('Migration: To strict follow the parentMessageId chain.');
for (let convo of conversations) {
const messages = await getMessages({
conversationId: convo.conversationId,
messageId: { $exists: false }
});
let model;
let oldId;
const promises = [];
messages.forEach((message, i) => {
const msgObj = message.toObject();
const newId = msgObj.id;
if (i === 0) {
message.parentMessageId = '00000000-0000-0000-0000-000000000000';
} else {
message.parentMessageId = oldId;
}
oldId = newId;
message.messageId = newId;
if (message.sender.toLowerCase() !== 'user' && !model) {
model = message.sender.toLowerCase();
}
if (message.sender.toLowerCase() === 'user') {
message.isCreatedByUser = true;
}
promises.push(message.save());
});
await Promise.all(promises);
await Conversation.findOneAndUpdate(
{ conversationId: convo.conversationId },
{ model },
{ new: true }
).exec();
}
try {
await mongoose.connection.db.collection('messages').dropIndex('id_1');
} catch (error) {
console.log("[Migrate] Index doesn't exist or already dropped");
}
} catch (error) {
console.log(error);
return { message: '[Migrate] Error migrating conversations' };
}
};
const migrateToSupportBetterCustomization = async () => {
try {
const conversations = await Conversation.find({ endpoint: null }).exec();
if (!conversations || conversations.length === 0) return { noNeed: true };
console.log('Migration: To support better customization.');
const promises = [];
for (let convo of conversations) {
const originalModel = convo?.model;
if (originalModel === 'chatgpt') {
convo.endpoint = 'openAI';
convo.model = 'gpt-3.5-turbo';
} else if (originalModel === 'chatgptCustom') {
convo.endpoint = 'openAI';
convo.model = 'gpt-3.5-turbo';
} else if (originalModel === 'bingai') {
convo.endpoint = 'bingAI';
convo.model = null;
convo.jailbreak = false;
} else if (originalModel === 'sydney') {
convo.endpoint = 'bingAI';
convo.model = null;
convo.jailbreak = true;
} else if (originalModel === 'chatgptBrowser') {
convo.endpoint = 'chatGPTBrowser';
convo.model = 'text-davinci-002-render-sha';
convo.jailbreak = true;
} else {
convo.endpoint = 'openAI';
convo.model = 'gpt-3.5-turbo';
}
promises.push(convo.save());
}
await Promise.all(promises);
} catch (error) {
console.log(error);
return { message: '[Migrate] Error migrating conversations' };
}
};
async function migrateDb() {
let ret = [];
ret[0] = await migrateToStrictFollowParentMessageIdChain();
ret[1] = await migrateToSupportBetterCustomization();
const isMigrated = !!ret.find((element) => !element?.noNeed);
if (!isMigrated) console.log('[Migrate] Nothing to migrate');
}
module.exports = migrateDb;

View File

@@ -1,4 +1,4 @@
const citationRegex = /\[\^\d+?\^]/g;
const citationRegex = /\[\^\d+?\^\]/g;
const citeText = (res, noLinks = false) => {
let result = res.text || res;
@@ -8,6 +8,7 @@ const citeText = (res, noLinks = false) => {
if (noLinks) {
citations.forEach((citation) => {
const digit = citation.match(/\d+?/g)[0];
// result = result.replaceAll(citation, `<sup>[${digit}](#) </sup>`);
result = result.replaceAll(citation, `<sup>[${digit}](#) </sup>`);
});
@@ -20,7 +21,8 @@ const citeText = (res, noLinks = false) => {
citations.forEach((citation) => {
const digit = citation.match(/\d+?/g)[0];
result = result.replaceAll(citation, `<sup>[${digit}](${sources[digit - 1]}) </sup>`);
result = result.replaceAll(citation, `<sup>[${digit}](${sources[digit - 1]}) </sup>`);
// result = result.replaceAll(citation, `<sup>[${digit}](${sources[digit - 1]}) </sup>`);
});
return result;

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

@@ -0,0 +1,29 @@
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);
}
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++]);
} else {
result.push(rightArr[rightIndex++]);
}
}
return result.concat(leftArr.slice(leftIndex)).concat(rightArr.slice(rightIndex));
}
module.exports = mergeSort;

15
api/lib/utils/misc.js Normal file
View File

@@ -0,0 +1,15 @@
const cleanUpPrimaryKeyValue = (value) => {
// For Bing convoId handling
return value.replace(/--/g, '|');
};
function replaceSup(text) {
if (!text.includes('<sup>')) return text;
const replacedText = text.replace(/<sup>/g, '^').replace(/\s+<\/sup>/g, '^');
return replacedText;
}
module.exports = {
cleanUpPrimaryKeyValue,
replaceSup
};

View File

@@ -0,0 +1,59 @@
const mergeSort = require('./mergeSort');
const { cleanUpPrimaryKeyValue } = require('./misc');
function reduceMessages(hits) {
const counts = {};
for (const hit of hits) {
if (!counts[hit.conversationId]) {
counts[hit.conversationId] = 1;
} else {
counts[hit.conversationId]++;
}
}
const result = [];
for (const [conversationId, count] of Object.entries(counts)) {
result.push({
conversationId,
count
});
}
return mergeSort(result, (a, b) => b.count - a.count);
}
function reduceHits(hits, titles = []) {
const counts = {};
const titleMap = {};
const convos = [...hits, ...titles];
for (const convo of convos) {
const currentId = cleanUpPrimaryKeyValue(convo.conversationId);
if (!counts[currentId]) {
counts[currentId] = 1;
} else {
counts[currentId]++;
}
if (convo.title) {
// titleMap[currentId] = convo._formatted.title;
titleMap[currentId] = convo.title;
}
}
const result = [];
for (const [conversationId, count] of Object.entries(counts)) {
result.push({
conversationId,
count,
title: titleMap[conversationId] ? titleMap[conversationId] : null
});
}
return mergeSort(result, (a, b) => b.count - a.count);
}
module.exports = { reduceMessages, reduceHits };

View File

@@ -0,0 +1,5 @@
const passport = require('passport');
const requireJwtAuth = passport.authenticate('jwt', { session: false });
module.exports = requireJwtAuth;

View File

@@ -0,0 +1,31 @@
const passport = require('passport');
const DebugControl = require('../utils/debug.js');
function log({ title, parameters }) {
DebugControl.log.functionName(title);
if (parameters) {
DebugControl.log.parameters(parameters);
}
}
const requireLocalAuth = (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) {
log({
title: '(requireLocalAuth) Error at passport.authenticate',
parameters: [{ name: 'error', value: err }]
});
return next(err);
}
if (!user) {
log({
title: '(requireLocalAuth) Error: No user'
});
return res.status(422).send(info);
}
req.user = user;
next();
})(req, res, next);
};
module.exports = requireLocalAuth;

84
api/models/Config.js Normal file
View File

@@ -0,0 +1,84 @@
const mongoose = require('mongoose');
const major = [0, 0];
const minor = [0, 0];
const patch = [0, 5];
const configSchema = mongoose.Schema(
{
tag: {
type: String,
required: true,
validate: {
validator: function (tag) {
const [part1, part2, part3] = tag.replace('v', '').split('.').map(Number);
// Check if all parts are numbers
if (isNaN(part1) || isNaN(part2) || isNaN(part3)) {
return false;
}
// Check if all parts are within their respective ranges
if (part1 < major[0] || part1 > major[1]) {
return false;
}
if (part2 < minor[0] || part2 > minor[1]) {
return false;
}
if (part3 < patch[0] || part3 > patch[1]) {
return false;
}
return true;
},
message: 'Invalid tag value'
}
},
searchEnabled: {
type: Boolean,
default: false
},
usersEnabled: {
type: Boolean,
default: false
},
startupCounts: {
type: Number,
default: 0
}
},
{ timestamps: true }
);
// Instance method
configSchema.methods.incrementCount = function () {
this.startupCounts += 1;
};
// Static methods
configSchema.statics.findByTag = async function (tag) {
return await this.findOne({ tag });
};
configSchema.statics.updateByTag = async function (tag, update) {
return await this.findOneAndUpdate({ tag }, update, { new: true });
};
const Config = mongoose.models.Config || mongoose.model('Config', configSchema);
module.exports = {
getConfigs: async (filter) => {
try {
return await Config.find(filter).exec();
} catch (error) {
console.error(error);
return { config: 'Error getting configs' };
}
},
deleteConfigs: async (filter) => {
try {
return await Config.deleteMany(filter).exec();
} catch (error) {
console.error(error);
return { config: 'Error deleting configs' };
}
}
};

View File

@@ -1,55 +1,10 @@
const mongoose = require('mongoose');
// const { Conversation } = require('./plugins');
const Conversation = require('./schema/convoSchema');
const { getMessages, deleteMessages } = require('./Message');
const convoSchema = mongoose.Schema({
conversationId: {
type: String,
unique: true,
required: true
},
parentMessageId: {
type: String,
required: true
},
title: {
type: String,
default: 'New conversation'
},
jailbreakConversationId: {
type: String
},
conversationSignature: {
type: String
},
clientId: {
type: String
},
invocationId: {
type: String
},
chatGptLabel: {
type: String
},
promptPrefix: {
type: String
},
model: {
type: String
},
suggestions: [{ type: String }],
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }],
created: {
type: Date,
default: Date.now
}
});
const Conversation =
mongoose.models.Conversation || mongoose.model('Conversation', convoSchema);
const getConvo = async (conversationId) => {
const getConvo = async (user, conversationId) => {
try {
return await Conversation.findOne({ conversationId }).exec();
return await Conversation.findOne({ user, conversationId }).exec();
} catch (error) {
console.log(error);
return { message: 'Error getting single conversation' };
@@ -57,66 +12,117 @@ const getConvo = async (conversationId) => {
};
module.exports = {
saveConvo: async ({ conversationId, title, ...convo }) => {
Conversation,
saveConvo: async (user, { conversationId, newConversationId, ...convo }) => {
try {
const messages = await getMessages({ conversationId });
const update = { ...convo, messages };
if (title) {
update.title = title;
const update = { ...convo, messages, user };
if (newConversationId) {
update.conversationId = newConversationId;
}
return await Conversation.findOneAndUpdate(
{ conversationId },
{ $set: update },
{ new: true, upsert: true }
).exec();
return await Conversation.findOneAndUpdate({ conversationId: conversationId, user }, update, {
new: true,
upsert: true
}).exec();
} catch (error) {
console.log(error);
return { message: 'Error saving conversation' };
}
},
updateConvo: async ({ conversationId, ...update }) => {
getConvosByPage: async (user, pageNumber = 1, pageSize = 14) => {
try {
return await Conversation.findOneAndUpdate({ conversationId }, update, {
new: true
}).exec();
} catch (error) {
console.log(error);
return { message: 'Error updating conversation' };
}
},
// getConvos: async () => await Conversation.find({}).sort({ created: -1 }).exec(),
getConvos: async (pageNumber = 1, pageSize = 12) => {
try {
const skip = (pageNumber - 1) * pageSize;
// const limit = pageNumber * pageSize;
const conversations = await Conversation.find({})
.sort({ created: -1 })
.skip(skip)
// .limit(limit)
const totalConvos = (await Conversation.countDocuments({ user })) || 1;
const totalPages = Math.ceil(totalConvos / pageSize);
const convos = await Conversation.find({ user })
.sort({ createdAt: -1, created: -1 })
.skip((pageNumber - 1) * pageSize)
.limit(pageSize)
.exec();
return conversations;
return { conversations: convos, pages: totalPages, pageNumber, pageSize };
} catch (error) {
console.log(error);
return { message: 'Error getting conversations' };
}
},
getConvo,
getConvoTitle: async (conversationId) => {
getConvosQueried: async (user, convoIds, pageNumber = 1, pageSize = 14) => {
try {
const convo = await getConvo(conversationId);
return convo.title;
if (!convoIds || convoIds.length === 0) {
return { conversations: [], pages: 1, pageNumber, pageSize };
}
const cache = {};
const convoMap = {};
const promises = [];
// will handle a syncing solution soon
const deletedConvoIds = [];
convoIds.forEach((convo) =>
promises.push(
Conversation.findOne({
user,
conversationId: convo.conversationId
}).exec()
)
);
const results = (await Promise.all(promises)).filter((convo, i) => {
if (!convo) {
deletedConvoIds.push(convoIds[i].conversationId);
return false;
} else {
const page = Math.floor(i / pageSize) + 1;
if (!cache[page]) {
cache[page] = [];
}
cache[page].push(convo);
convoMap[convo.conversationId] = convo;
return true;
}
});
// const startIndex = (pageNumber - 1) * pageSize;
// const convos = results.slice(startIndex, startIndex + pageSize);
const totalPages = Math.ceil(results.length / pageSize);
cache.pages = totalPages;
cache.pageSize = pageSize;
return {
cache,
conversations: cache[pageNumber] || [],
pages: totalPages || 1,
pageNumber,
pageSize,
// will handle a syncing solution soon
filter: new Set(deletedConvoIds),
convoMap
};
} catch (error) {
console.log(error);
return { message: 'Error fetching conversations' };
}
},
getConvo,
/* chore: this method is not properly error handled */
getConvoTitle: async (user, conversationId) => {
try {
const convo = await getConvo(user, conversationId);
/* ChatGPT Browser was triggering error here due to convo being saved later */
if (convo && !convo.title) {
return null;
} else {
// TypeError: Cannot read properties of null (reading 'title')
return convo?.title || 'New Chat';
}
} catch (error) {
console.log(error);
return { message: 'Error getting conversation title' };
}
},
deleteConvos: async (filter) => {
let deleteCount = await Conversation.deleteMany(filter).exec();
deleteCount.messages = await deleteMessages(filter);
deleteConvos: async (user, filter) => {
let toRemove = await Conversation.find({ ...filter, user }).select('conversationId');
const ids = toRemove.map((instance) => instance.conversationId);
let deleteCount = await Conversation.deleteMany({ ...filter, user }).exec();
deleteCount.messages = await deleteMessages({ conversationId: { $in: ids } });
return deleteCount;
}
};

View File

@@ -1,82 +0,0 @@
const mongoose = require('mongoose');
const customGptSchema = mongoose.Schema({
chatGptLabel: {
type: String,
required: true
},
promptPrefix: {
type: String
},
value: {
type: String,
required: true
},
created: {
type: Date,
default: Date.now
}
});
const CustomGpt = mongoose.models.CustomGpt || mongoose.model('CustomGpt', customGptSchema);
const createCustomGpt = async ({ chatGptLabel, promptPrefix, value }) => {
try {
await CustomGpt.create({
chatGptLabel,
promptPrefix,
value
});
return { chatGptLabel, promptPrefix, value };
} catch (error) {
console.error(error);
return { customGpt: 'Error saving customGpt' };
}
};
module.exports = {
getCustomGpts: async (filter) => {
try {
return await CustomGpt.find(filter).exec();
} catch (error) {
console.error(error);
return { customGpt: 'Error getting customGpts' };
}
},
updateCustomGpt: async ({ value, ...update }) => {
try {
const customGpt = await CustomGpt.findOne({ value }).exec();
if (!customGpt) {
return await createCustomGpt({ value, ...update });
} else {
return await CustomGpt.findOneAndUpdate({ value }, update, {
new: true,
upsert: true
}).exec();
}
} catch (error) {
console.log(error);
return { message: 'Error updating customGpt' };
}
},
updateByLabel: async ({ prevLabel, ...update }) => {
try {
return await CustomGpt.findOneAndUpdate({ chatGptLabel: prevLabel }, update, {
new: true,
upsert: true
}).exec();
} catch (error) {
console.log(error);
return { message: 'Error updating customGpt' };
}
},
deleteCustomGpts: async (filter) => {
try {
return await CustomGpt.deleteMany(filter).exec();
} catch (error) {
console.error(error);
return { customGpt: 'Error deleting customGpts' };
}
}
};

View File

@@ -1,75 +1,82 @@
const mongoose = require('mongoose');
const messageSchema = mongoose.Schema({
id: {
type: String,
unique: true,
required: true
},
conversationId: {
type: String,
required: true
},
conversationSignature: {
type: String,
// required: true
},
clientId: {
type: String,
},
invocationId: {
type: String,
},
parentMessageId: {
type: String,
// required: true
},
sender: {
type: String,
required: true
},
text: {
type: String,
required: true
},
created: {
type: Date,
default: Date.now
}
});
const Message = mongoose.models.Message || mongoose.model('Message', messageSchema);
const Message = require('./schema/messageSchema');
module.exports = {
saveMessage: async ({ id, conversationId, parentMessageId, sender, text }) => {
Message,
async saveMessage({
messageId,
newMessageId,
conversationId,
parentMessageId,
sender,
text,
isCreatedByUser = false,
error,
unfinished,
cancelled
}) {
try {
await Message.create({
id,
// may also need to update the conversation here
await Message.findOneAndUpdate(
{ messageId },
{
messageId: newMessageId || messageId,
conversationId,
parentMessageId,
sender,
text,
isCreatedByUser,
error,
unfinished,
cancelled
},
{ upsert: true, new: true }
);
return {
messageId,
conversationId,
parentMessageId,
sender,
text
});
return { id, conversationId, parentMessageId, sender, text };
} catch (error) {
console.error(error);
return { message: 'Error saving message' };
text,
isCreatedByUser
};
} catch (err) {
console.error(`Error saving message: ${err}`);
throw new Error('Failed to save message.');
}
},
getMessages: async (filter) => {
async deleteMessagesSince({ messageId, conversationId }) {
try {
return await Message.find(filter).exec()
} catch (error) {
console.error(error);
return { message: 'Error getting messages' };
const message = await Message.findOne({ messageId }).exec();
if (message) {
return await Message.find({ conversationId })
.deleteMany({ createdAt: { $gt: message.createdAt } })
.exec();
}
} catch (err) {
console.error(`Error deleting messages: ${err}`);
throw new Error('Failed to delete messages.');
}
},
deleteMessages: async (filter) => {
async getMessages(filter) {
try {
return await Message.deleteMany(filter).exec()
} catch (error) {
console.error(error);
return { message: 'Error deleting messages' };
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.');
}
}
}
};

46
api/models/Preset.js Normal file
View File

@@ -0,0 +1,46 @@
const Preset = require('./schema/presetSchema');
const getPreset = async (user, presetId) => {
try {
return await Preset.findOne({ user, presetId }).exec();
} catch (error) {
console.log(error);
return { message: 'Error getting single preset' };
}
};
module.exports = {
Preset,
getPreset,
getPresets: async (user, filter) => {
try {
return await Preset.find({ ...filter, user }).exec();
} catch (error) {
console.log(error);
return { message: 'Error retriving presets' };
}
},
savePreset: async (user, { presetId, newPresetId, ...preset }) => {
try {
const update = { presetId, ...preset };
if (newPresetId) {
update.presetId = newPresetId;
}
return await Preset.findOneAndUpdate(
{ presetId, user },
{ $set: update },
{ new: true, upsert: true }
).exec();
} catch (error) {
console.log(error);
return { message: 'Error saving preset' };
}
},
deletePresets: async (user, filter) => {
let toRemove = await Preset.find({ ...filter, user }).select('presetId');
const ids = toRemove.map((instance) => instance.presetId);
let deleteCount = await Preset.deleteMany({ ...filter, user }).exec();
return deleteCount;
}
};

View File

@@ -1,22 +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,
},
created: {
type: Date,
default: Date.now
}
});
{ timestamps: true }
);
const Prompt = mongoose.models.Prompt || mongoose.model('Prompt', promptSchema);
@@ -35,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' };
@@ -43,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' };
}
}
}
};

176
api/models/User.js Normal file
View File

@@ -0,0 +1,176 @@
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const Joi = require('joi');
const DebugControl = require('../utils/debug.js');
function log({ title, parameters }) {
DebugControl.log.functionName(title);
DebugControl.log.parameters(parameters);
}
const Session = mongoose.Schema({
refreshToken: {
type: String,
default: ''
}
});
const userSchema = mongoose.Schema(
{
name: {
type: String
},
username: {
type: String,
lowercase: true,
required: [true, "can't be blank"],
match: [/^[a-zA-Z0-9_]+$/, 'is invalid'],
index: true
},
email: {
type: String,
required: [true, "can't be blank"],
lowercase: true,
unique: true,
match: [/\S+@\S+\.\S+/, 'is invalid'],
index: true
},
emailVerified: {
type: Boolean,
required: true,
default: false
},
password: {
type: String,
trim: true,
minlength: 8,
maxlength: 60
},
avatar: {
type: String,
required: false
},
provider: {
type: String,
required: true,
default: 'local'
},
role: {
type: String,
default: 'USER'
},
googleId: {
type: String,
unique: true,
sparse: true
},
facebookId: {
type: String,
unique: true,
sparse: true
},
refreshToken: {
type: [Session]
}
},
{ timestamps: true }
);
//Remove refreshToken from the response
userSchema.set('toJSON', {
transform: function (_doc, ret,) {
delete ret.refreshToken;
return ret;
}
});
userSchema.methods.toJSON = function () {
return {
id: this._id,
provider: this.provider,
email: this.email,
name: this.name,
username: this.username,
avatar: this.avatar,
role: this.role,
emailVerified: this.emailVerified,
createdAt: this.createdAt,
updatedAt: this.updatedAt
};
};
const isProduction = process.env.NODE_ENV === 'production';
const secretOrKey = isProduction ? process.env.JWT_SECRET_PROD : process.env.JWT_SECRET_DEV;
const refreshSecret = isProduction
? process.env.REFRESH_TOKEN_SECRET_PROD
: process.env.REFRESH_TOKEN_SECRET_DEV;
userSchema.methods.generateToken = function () {
const token = jwt.sign(
{
id: this._id,
username: this.username,
provider: this.provider,
email: this.email
},
secretOrKey,
{ expiresIn: eval(process.env.SESSION_EXPIRY) }
);
return token;
};
userSchema.methods.generateRefreshToken = function () {
const refreshToken = jwt.sign(
{
id: this._id,
username: this.username,
provider: this.provider,
email: this.email
},
refreshSecret,
{ expiresIn: eval(process.env.REFRESH_TOKEN_EXPIRY) }
);
return refreshToken;
};
userSchema.methods.comparePassword = function (candidatePassword, callback) {
bcrypt.compare(candidatePassword, this.password, (err, isMatch) => {
if (err) return callback(err);
callback(null, isMatch);
});
};
module.exports.hashPassword = async (password) => {
const hashedPassword = await new Promise((resolve, reject) => {
bcrypt.hash(password, 10, function (err, hash) {
if (err) reject(err);
else resolve(hash);
});
});
return hashedPassword;
};
module.exports.validateUser = (user) => {
log({
title: 'Validate User',
parameters: [{ name: 'Validate User', value: user }]
});
const schema = {
avatar: Joi.any(),
name: Joi.string().min(2).max(80).required(),
username: Joi.string()
.min(2)
.max(80)
.regex(/^[a-zA-Z0-9_]+$/)
.required(),
password: Joi.string().min(8).max(60).allow('').allow(null)
};
return schema.validate(user);
};
const User = mongoose.model('User', userSchema);
module.exports = User;

View File

@@ -1,15 +1,19 @@
const { saveMessage, deleteMessages } = require('./Message');
const { getCustomGpts, updateCustomGpt, updateByLabel, deleteCustomGpts } = require('./CustomGpt');
const { getMessages, saveMessage, deleteMessagesSince, deleteMessages } = require('./Message');
const { getConvoTitle, getConvo, saveConvo } = require('./Conversation');
const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset');
module.exports = {
getMessages,
saveMessage,
deleteMessagesSince,
deleteMessages,
getConvoTitle,
getConvo,
saveConvo,
getCustomGpts,
updateCustomGpt,
updateByLabel,
deleteCustomGpts
getPreset,
getPresets,
savePreset,
deletePresets
};

View File

@@ -0,0 +1,208 @@
const mongoose = require('mongoose');
const { MeiliSearch } = require('meilisearch');
const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc');
const _ = require('lodash');
const validateOptions = function (options) {
const requiredKeys = ['host', 'apiKey', 'indexName'];
requiredKeys.forEach((key) => {
if (!options[key]) throw new Error(`Missing mongoMeili Option: ${key}`);
});
};
const createMeiliMongooseModel = function ({ index, indexName, client, attributesToIndex }) {
// console.log('attributesToIndex', attributesToIndex);
const primaryKey = attributesToIndex[0];
// MeiliMongooseModel is of type Mongoose.Model
class MeiliMongooseModel {
// Clear Meili index
static async clearMeiliIndex() {
await index.delete();
// await index.deleteAllDocuments();
await this.collection.updateMany({ _meiliIndex: true }, { $set: { _meiliIndex: false } });
}
static async resetIndex() {
await this.clearMeiliIndex();
await client.createIndex(indexName, { primaryKey });
}
// Clear Meili index
// Push a mongoDB collection to Meili index
static async syncWithMeili() {
await this.resetIndex();
// const docs = await this.find();
const docs = await this.find({ _meiliIndex: { $in: [null, false] } });
console.log('docs', docs.length);
await Promise.all(
docs.map(function (doc) {
return doc.addObjectToMeili();
})
);
}
// Set one or more settings of the meili index
static async setMeiliIndexSettings(settings) {
return await index.updateSettings(settings);
}
// Search the index
static async meiliSearch(q, params, populate) {
const data = await index.search(q, params);
// Populate hits with content from mongodb
if (populate) {
// 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]));
// console.log('query', query);
const hitsFromMongoose = await this.find(
query,
_.reduce(
this.schema.obj,
function (results, value, key) {
return { ...results, [key]: 1 };
},
{ _id: 1 }
)
);
// Add additional data from mongodb into Meili search hits
const populatedHits = data.hits.map(function (hit) {
const query = {};
query[primaryKey] = hit[primaryKey];
const originalHit = _.find(hitsFromMongoose, query);
return {
...(originalHit ? originalHit.toJSON() : {}),
...hit
};
});
data.hits = populatedHits;
}
return data;
}
// Push new document to Meili
async addObjectToMeili() {
const object = _.pick(this.toJSON(), attributesToIndex);
// NOTE: MeiliSearch does not allow | in primary key, so we replace it with - for Bing convoIds
// object.conversationId = object.conversationId.replace(/\|/g, '-');
if (object.conversationId && object.conversationId.includes('|')) {
object.conversationId = object.conversationId.replace(/\|/g, '--');
}
try {
// console.log('Adding document to Meili', object);
await index.addDocuments([object]);
} catch (error) {
// console.log('Error adding document to Meili');
// console.error(error);
}
await this.collection.updateMany({ _id: this._id }, { $set: { _meiliIndex: true } });
}
// Update an existing document in Meili
async updateObjectToMeili() {
const object = _.pick(this.toJSON(), attributesToIndex);
await index.updateDocuments([object]);
}
// Delete a document from Meili
async deleteObjectFromMeili() {
await index.deleteDocument(this._id);
}
// * schema.post('save')
postSaveHook() {
if (this._meiliIndex) {
this.updateObjectToMeili();
} else {
this.addObjectToMeili();
}
}
// * schema.post('update')
postUpdateHook() {
if (this._meiliIndex) {
this.updateObjectToMeili();
}
}
// * schema.post('remove')
postRemoveHook() {
if (this._meiliIndex) {
this.deleteObjectFromMeili();
}
}
}
return MeiliMongooseModel;
};
module.exports = function mongoMeili(schema, options) {
// Vaidate Options for mongoMeili
validateOptions(options);
// Add meiliIndex to schema
schema.add({
_meiliIndex: {
type: Boolean,
required: false,
select: false,
default: false
}
});
const { host, apiKey, indexName, primaryKey } = options;
// Setup MeiliSearch Client
const client = new MeiliSearch({ host, apiKey });
// Asynchronously create the index
client.createIndex(indexName, { primaryKey });
// Setup the index to search for this schema
const index = client.index(indexName);
const attributesToIndex = [
..._.reduce(
schema.obj,
function (results, value, key) {
return value.meiliIndex ? [...results, key] : results;
// }, []), '_id'];
},
[]
)
];
schema.loadClass(createMeiliMongooseModel({ index, indexName, client, attributesToIndex }));
// Register hooks
schema.post('save', function (doc) {
doc.postSaveHook();
});
schema.post('update', function (doc) {
doc.postUpdateHook();
});
schema.post('remove', function (doc) {
doc.postRemoveHook();
});
schema.post('deleteMany', function () {
// console.log('deleteMany hook', doc);
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();
}
});
schema.post('findOneAndUpdate', function (doc) {
doc.postSaveHook();
});
};

View File

@@ -0,0 +1,84 @@
module.exports = {
// endpoint: [azureOpenAI, openAI, bingAI, chatGPTBrowser]
endpoint: {
type: String,
default: null,
required: true
},
// for azureOpenAI, openAI, chatGPTBrowser only
model: {
type: String,
default: null,
required: false
},
// for azureOpenAI, openAI only
chatGptLabel: {
type: String,
default: null,
required: false
},
// for google only
modelLabel: {
type: String,
default: null,
required: false
},
promptPrefix: {
type: String,
default: null,
required: false
},
temperature: {
type: Number,
default: 1,
required: false
},
top_p: {
type: Number,
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,
required: false
},
frequency_penalty: {
type: Number,
default: 0,
required: false
},
// for bingai only
jailbreak: {
type: Boolean,
default: false
},
context: {
type: String,
default: null
},
systemMessage: {
type: String,
default: null
},
toneStyle: {
type: String,
default: null
}
};

View File

@@ -0,0 +1,58 @@
const mongoose = require('mongoose');
const mongoMeili = require('../plugins/mongoMeili');
const conversationPreset = require('./conversationPreset');
const convoSchema = mongoose.Schema(
{
conversationId: {
type: String,
unique: true,
required: true,
index: true,
meiliIndex: true
},
title: {
type: String,
default: 'New Chat',
meiliIndex: true
},
user: {
type: String,
default: null
},
messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }],
// google only
examples: [{ type: mongoose.Schema.Types.Mixed }],
...conversationPreset,
// for bingAI only
jailbreakConversationId: {
type: String,
default: null
},
conversationSignature: {
type: String,
default: null
},
clientId: {
type: String,
default: null
},
invocationId: {
type: Number,
default: 1
}
},
{ timestamps: true }
);
if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
convoSchema.plugin(mongoMeili, {
host: process.env.MEILI_HOST,
apiKey: process.env.MEILI_MASTER_KEY,
indexName: 'convos', // Will get created automatically if it doesn't exist already
primaryKey: 'conversationId'
});
}
const Conversation = mongoose.models.Conversation || mongoose.model('Conversation', convoSchema);
module.exports = Conversation;

View File

@@ -0,0 +1,79 @@
const mongoose = require('mongoose');
const mongoMeili = require('../plugins/mongoMeili');
const messageSchema = mongoose.Schema(
{
messageId: {
type: String,
unique: true,
required: true,
index: true,
meiliIndex: true
},
conversationId: {
type: String,
required: true,
meiliIndex: true
},
conversationSignature: {
type: String
// required: true
},
clientId: {
type: String
},
invocationId: {
type: String
},
parentMessageId: {
type: String
// required: true
},
sender: {
type: String,
required: true,
meiliIndex: true
},
text: {
type: String,
required: true,
meiliIndex: true
},
isCreatedByUser: {
type: Boolean,
required: true,
default: false
},
unfinished: {
type: Boolean,
default: false
},
cancelled: {
type: Boolean,
default: false
},
error: {
type: Boolean,
default: false
},
_meiliIndex: {
type: Boolean,
required: false,
select: false,
default: false
}
},
{ timestamps: true }
);
if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
messageSchema.plugin(mongoMeili, {
host: process.env.MEILI_HOST,
apiKey: process.env.MEILI_MASTER_KEY,
indexName: 'messages',
primaryKey: 'messageId'
});
}
const Message = mongoose.models.Message || mongoose.model('Message', messageSchema);
module.exports = Message;

View File

@@ -0,0 +1,29 @@
const mongoose = require('mongoose');
const conversationPreset = require('./conversationPreset');
const presetSchema = mongoose.Schema(
{
presetId: {
type: String,
unique: true,
required: true,
index: true
},
title: {
type: String,
default: 'New Chat',
meiliIndex: true
},
user: {
type: String,
default: null
},
// google only
examples: [{ type: mongoose.Schema.Types.Mixed }],
...conversationPreset
},
{ timestamps: true }
);
const Preset = mongoose.models.Preset || mongoose.model('Preset', presetSchema);
module.exports = Preset;

View File

@@ -0,0 +1,22 @@
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const tokenSchema = new Schema({
userId: {
type: Schema.Types.ObjectId,
required: true,
ref: 'user'
},
token: {
type: String,
required: true
},
createdAt: {
type: Date,
required: true,
default: Date.now,
expires: 900
}
});
module.exports = mongoose.model('Token', tokenSchema);

7893
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": "1.0.0",
"name": "chat-backend",
"version": "0.4.6",
"description": "",
"main": "server/index.js",
"scripts": {
@@ -19,16 +19,38 @@
},
"homepage": "https://github.com/danny-avila/chatgpt-clone#readme",
"dependencies": {
"@dqbd/tiktoken": "^1.0.2",
"@keyv/mongo": "^2.1.8",
"@vscode/vscode-languagedetection": "^1.0.22",
"@waylaidwanderer/chatgpt-api": "^1.28.2",
"@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",
"express": "^4.18.2",
"googleapis": "^118.0.0",
"handlebars": "^4.7.7",
"html": "^1.0.0",
"joi": "^17.9.2",
"jsonwebtoken": "^9.0.0",
"keyv": "^4.5.2",
"keyv-file": "^0.2.0",
"mongoose": "^6.9.0",
"openai": "^3.1.0"
"lodash": "^4.17.21",
"meilisearch": "^0.32.3",
"mongoose": "^7.1.1",
"nodemailer": "^6.9.1",
"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": {
"nodemon": "^2.0.20",

View File

@@ -0,0 +1,168 @@
const {
loginUser,
logoutUser,
registerUser,
requestPasswordReset,
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) {
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 {
return res.status(400).json({ message: 'Invalid credentials' });
}
} catch (err) {
console.log(err);
return res.status(500).json({ message: err.message });
}
};
const logoutController = async (req, res) => {
const { signedCookies = {} } = req;
const { refreshToken } = signedCookies;
try {
const logout = await logoutUser(req.user, refreshToken);
console.log(logout);
const { status, message } = logout;
if (status === 200) {
res.clearCookie('token');
res.clearCookie('refreshToken');
res.status(status).send({ message });
} else {
res.status(status).send({ message });
}
} catch (err) {
console.log(err);
return res.status(500).json({ message: err.message });
}
};
const registrationController = async (req, res) => {
try {
const response = await registerUser(req.body);
if (response.status === 200) {
const { status, user } = response;
const token = user.generateToken();
//send token for automatic login
res.cookie('token', token, {
expires: new Date(Date.now() + eval(process.env.SESSION_EXPIRY)),
httpOnly: false,
secure: isProduction
});
res.status(status).send({ user });
} else {
const { status, message } = response;
res.status(status).send({ message });
}
} catch (err) {
console.log(err);
return res.status(500).json({ message: err.message });
}
};
const getUserController = async (req, res) => {
return res.status(200).send(req.user);
};
const resetPasswordRequestController = async (req, res) => {
try {
const resetService = await requestPasswordReset(req.body.email);
if (resetService.link) {
return res.status(200).json(resetService);
} else {
return res.status(400).json(resetService);
}
} catch (e) {
console.log(e);
return res.status(400).json({ message: e.message });
}
};
const resetPasswordController = async (req, res) => {
try {
const resetPasswordService = await resetPassword(
req.body.userId,
req.body.token,
req.body.password
);
if (resetPasswordService instanceof Error) {
return res.status(400).json(resetPasswordService);
} else {
return res.status(200).json(resetPasswordService);
}
} catch (e) {
console.log(e);
return res.status(400).json({ message: e.message });
}
};
const refreshController = async (req, res, next) => {
const { signedCookies = {} } = req;
const { refreshToken } = signedCookies;
//TODO
// if (refreshToken) {
// try {
// const payload = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
// const userId = payload._id;
// User.findOne({ _id: userId }).then(
// (user) => {
// if (user) {
// // Find the refresh token against the user record in database
// const tokenIndex = user.refreshToken.findIndex(item => item.refreshToken === refreshToken);
// if (tokenIndex === -1) {
// res.statusCode = 401;
// res.send('Unauthorized');
// } else {
// const token = req.user.generateToken();
// // If the refresh token exists, then create new one and replace it.
// const newRefreshToken = req.user.generateRefreshToken();
// user.refreshToken[tokenIndex] = { refreshToken: newRefreshToken };
// user.save((err) => {
// if (err) {
// res.statusCode = 500;
// res.send(err);
// } else {
// // setTokenCookie(res, newRefreshToken);
// const user = req.user.toJSON();
// res.status(200).send({ token, user });
// }
// });
// }
// } else {
// res.statusCode = 401;
// res.send('Unauthorized');
// }
// },
// err => next(err)
// );
// } catch (err) {
// res.statusCode = 401;
// res.send('Unauthorized');
// }
// } else {
// res.statusCode = 401;
// res.send('Unauthorized');
// }
};
module.exports = {
getUserController,
loginController,
logoutController,
refreshController,
registrationController,
resetPasswordRequestController,
resetPasswordController
};

View File

@@ -0,0 +1,33 @@
//handle duplicates
const handleDuplicateKeyError = (err, res) => {
const field = Object.keys(err.keyValue);
const code = 409;
const error = `An document with that ${field} already exists.`;
console.log('congrats you hit the duped keys error');
res.status(code).send({ messages: error, fields: field });
};
//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 code = 400;
if (errors.length > 1) {
const formattedErrors = errors.join(' ');
res.status(code).send({ messages: formattedErrors, fields: fields });
} else {
res.status(code).send({ messages: errors, fields: fields });
}
};
// eslint-disable-next-line no-unused-vars
module.exports = (err, req, res, next) => {
try {
console.log('congrats you hit the error middleware');
if (err.name === 'ValidationError') return (err = handleValidationError(err, res));
if (err.code && err.code == 11000) return (err = handleDuplicateKeyError(err, res));
} catch (err) {
res.status(500).send('An unknown error occurred.');
}
};

View File

@@ -1,32 +1,80 @@
const express = require('express');
const dbConnect = require('../models/dbConnect');
const connectDb = require('../lib/db/connectDb');
const migrateDb = require('../lib/db/migrateDb');
const indexSync = require('../lib/db/indexSync');
const path = require('path');
const cors = require('cors');
const routes = require('./routes');
const app = express();
const errorController = require('./controllers/error.controller');
const passport = require('passport');
const port = process.env.PORT || 3080;
const host = process.env.HOST || 'localhost'
const host = process.env.HOST || 'localhost';
const projectPath = path.join(__dirname, '..', '..', 'client');
dbConnect().then(() => console.log('Connected to MongoDB'));
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(projectPath, 'public')));
(async () => {
await connectDb();
console.log('Connected to MongoDB');
await migrateDb();
await indexSync();
app.get('/', function (req, res) {
console.log(path.join(projectPath, 'public', 'index.html'));
res.sendFile(path.join(projectPath, 'public', 'index.html'));
const app = express();
app.use(errorController);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(projectPath, 'dist')));
app.set('trust proxy', 1); // trust first proxy
app.use(cors());
// OAUTH
app.use(passport.initialize());
require('../strategies/jwtStrategy');
require('../strategies/localStrategy');
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) {
require('../strategies/facebookStrategy');
}
app.use('/oauth', routes.oauth);
// api endpoint
app.use('/api/auth', routes.auth);
app.use('/api/search', routes.search);
app.use('/api/ask', routes.ask);
app.use('/api/messages', routes.messages);
app.use('/api/convos', routes.convos);
app.use('/api/presets', routes.presets);
app.use('/api/prompts', routes.prompts);
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'));
});
app.listen(port, host, () => {
if (host == '0.0.0.0')
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}`);
});
})();
let messageCount = 0;
process.on('uncaughtException', (err) => {
if (!err.message.includes('fetch failed')) {
console.error('There was an uncaught error:', err.message);
}
if (err.message.includes('fetch failed')) {
if (messageCount === 0) {
console.error('Meilisearch error, search will be disabled');
messageCount++;
}
} else {
process.exit(1);
}
});
app.use('/api/ask', routes.ask);
app.use('/api/messages', routes.messages);
app.use('/api/convos', routes.convos);
app.use('/api/customGpts', routes.customGpts);
app.use('/api/prompts', routes.prompts);
app.listen(port, host, () => {
if (host=='0.0.0.0')
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}`);
});

View File

@@ -1,157 +0,0 @@
const express = require('express');
const crypto = require('crypto');
const router = express.Router();
const askBing = require('./askBing');
const askSydney = require('./askSydney');
const {
titleConvo,
askClient,
browserClient,
customClient,
detectCode
} = require('../../app/');
const { getConvo, saveMessage, deleteMessages, saveConvo } = require('../../models');
const { handleError, sendMessage } = require('./handlers');
router.use('/bing', askBing);
router.use('/sydney', askSydney);
router.post('/', async (req, res) => {
let { model, text, parentMessageId, conversationId, chatGptLabel, promptPrefix } = req.body;
if (text.length === 0) {
return handleError(res, 'Prompt empty or too short');
}
const userMessageId = crypto.randomUUID();
let userMessage = { id: userMessageId, sender: 'User', text };
console.log('ask log', {
model,
...userMessage,
parentMessageId,
conversationId,
chatGptLabel,
promptPrefix
});
let client;
if (model === 'chatgpt') {
client = askClient;
} else if (model === 'chatgptCustom') {
client = customClient;
} else {
client = browserClient;
}
if (model === 'chatgptCustom' && !chatGptLabel && conversationId) {
const convo = await getConvo({ conversationId });
if (convo) {
console.log('found convo for custom gpt', { convo })
chatGptLabel = convo.chatGptLabel;
promptPrefix = convo.promptPrefix;
}
}
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'
});
try {
let i = 0;
let tokens = '';
const progressCallback = async (partial) => {
if (i === 0 && typeof partial === 'object') {
userMessage.parentMessageId = parentMessageId ? parentMessageId : partial.id;
userMessage.conversationId = conversationId ? conversationId : partial.conversationId;
await saveMessage(userMessage);
sendMessage(res, { ...partial, initial: true });
i++;
}
if (typeof partial === 'object') {
sendMessage(res, { ...partial, message: true });
} else {
tokens += partial === text ? '' : partial;
if (tokens.match(/^\n/)) {
tokens = tokens.replace(/^\n/, '');
}
if (tokens.includes('[DONE]')) {
tokens = tokens.replace('[DONE]', '');
}
// tokens = await detectCode(tokens);
sendMessage(res, { text: tokens, message: true, initial: i === 0 ? true : false });
i++;
}
};
let gptResponse = await client({
text,
progressCallback,
convo: {
parentMessageId,
conversationId
},
chatGptLabel,
promptPrefix
});
console.log('CLIENT RESPONSE', gptResponse);
if (!gptResponse.parentMessageId) {
gptResponse.text = gptResponse.response;
gptResponse.id = gptResponse.messageId;
gptResponse.parentMessageId = gptResponse.messageId;
userMessage.parentMessageId = parentMessageId ? parentMessageId : gptResponse.messageId;
userMessage.conversationId = conversationId
? conversationId
: gptResponse.conversationId;
await saveMessage(userMessage);
delete gptResponse.response;
}
if (
(gptResponse.text.includes('2023') && !gptResponse.text.trim().includes(' ')) ||
gptResponse.text.toLowerCase().includes('no response') ||
gptResponse.text.toLowerCase().includes('no answer')
) {
return handleError(res, 'Prompt empty or too short');
}
if (!parentMessageId) {
gptResponse.title = await titleConvo({
model,
message: text,
response: JSON.stringify(gptResponse.text)
});
}
gptResponse.sender = model === 'chatgptCustom' ? chatGptLabel : model;
gptResponse.final = true;
gptResponse.text = await detectCode(gptResponse.text);
if (chatGptLabel?.length > 0 && model === 'chatgptCustom') {
gptResponse.chatGptLabel = chatGptLabel;
}
if (promptPrefix?.length > 0 && model === 'chatgptCustom') {
gptResponse.promptPrefix = promptPrefix;
}
await saveMessage(gptResponse);
await saveConvo(gptResponse);
sendMessage(res, gptResponse);
res.end();
} catch (error) {
console.log(error);
await deleteMessages({ id: userMessageId });
handleError(res, error.message);
}
});
module.exports = router;

View File

@@ -0,0 +1,64 @@
const Keyv = require('keyv');
const { KeyvFile } = require('keyv-file');
const addToCache = async ({ endpoint, endpointOption, userMessage, responseMessage }) => {
try {
const conversationsCache = new Keyv({
store: new KeyvFile({ filename: './data/cache.json' }),
namespace: 'chatgpt' // should be 'bing' for bing/sydney
});
const {
conversationId,
messageId: userMessageId,
parentMessageId: userParentMessageId,
text: userText
} = userMessage;
const {
messageId: responseMessageId,
parentMessageId: responseParentMessageId,
text: responseText
} = responseMessage;
let conversation = await conversationsCache.get(conversationId);
// used to generate a title for the conversation if none exists
// let isNewConversation = false;
if (!conversation) {
conversation = {
messages: [],
createdAt: Date.now()
};
// isNewConversation = true;
}
const roles = (options) => {
if (endpoint === 'openAI') {
return options?.chatGptLabel || 'ChatGPT';
} else if (endpoint === 'bingAI') {
return options?.jailbreak ? 'Sydney' : 'BingAI';
}
};
let _userMessage = {
id: userMessageId,
parentMessageId: userParentMessageId,
role: 'User',
message: userText
};
let _responseMessage = {
id: responseMessageId,
parentMessageId: responseParentMessageId,
role: roles(endpointOption),
message: responseText
};
conversation.messages.push(_userMessage, _responseMessage);
await conversationsCache.set(conversationId, conversation);
} catch (error) {
console.error('Trouble adding to cache', error);
}
};
module.exports = addToCache;

View File

@@ -0,0 +1,266 @@
const express = require('express');
const crypto = require('crypto');
const router = express.Router();
const { titleConvo, askBing } = require('../../../app');
const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('../../../models');
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
const requireJwtAuth = require('../../../middleware/requireJwtAuth');
router.post('/', requireJwtAuth, async (req, res) => {
const {
endpoint,
text,
messageId,
overrideParentMessageId = null,
parentMessageId,
conversationId: oldConversationId
} = req.body;
if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' });
if (endpoint !== 'bingAI') return handleError(res, { text: 'Illegal request' });
// build user message
const conversationId = oldConversationId || crypto.randomUUID();
const isNewConversation = !oldConversationId;
const userMessageId = messageId;
const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000';
let userMessage = {
messageId: userMessageId,
sender: 'User',
text,
parentMessageId: userParentMessageId,
conversationId,
isCreatedByUser: true
};
// build endpoint option
let endpointOption = {};
if (req.body?.jailbreak)
endpointOption = {
jailbreak: req.body?.jailbreak ?? false,
jailbreakConversationId: req.body?.jailbreakConversationId ?? null,
systemMessage: req.body?.systemMessage ?? null,
context: req.body?.context ?? null,
toneStyle: req.body?.toneStyle ?? 'fast',
token: req.body?.token ?? null
};
else
endpointOption = {
jailbreak: req.body?.jailbreak ?? false,
systemMessage: req.body?.systemMessage ?? null,
context: req.body?.context ?? null,
conversationSignature: req.body?.conversationSignature ?? null,
clientId: req.body?.clientId ?? null,
invocationId: req.body?.invocationId ?? null,
toneStyle: req.body?.toneStyle ?? 'fast',
token: req.body?.token ?? null
};
console.log('ask log', {
userMessage,
endpointOption,
conversationId
});
if (!overrideParentMessageId) {
await saveMessage(userMessage);
await saveConvo(req.user.id, {
...userMessage,
...endpointOption,
conversationId,
endpoint
});
}
// eslint-disable-next-line no-use-before-define
return await ask({
isNewConversation,
userMessage,
endpointOption,
conversationId,
preSendRequest: true,
overrideParentMessageId,
req,
res
});
});
const ask = async ({
isNewConversation,
userMessage,
endpointOption,
conversationId,
preSendRequest = true,
overrideParentMessageId = null,
req,
res
}) => {
let { text, parentMessageId: userParentMessageId, messageId: userMessageId } = userMessage;
let responseMessageId = crypto.randomUUID();
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'
});
if (preSendRequest) sendMessage(res, { message: userMessage, created: true });
try {
let lastSavedTimestamp = 0;
const { onProgress: progressCallback, getPartialText } = createOnProgress({
onProgress: ({ text }) => {
const currentTimestamp = Date.now();
if (currentTimestamp - lastSavedTimestamp > 500) {
lastSavedTimestamp = currentTimestamp;
saveMessage({
messageId: responseMessageId,
sender: endpointOption?.jailbreak ? 'Sydney' : 'BingAI',
conversationId,
parentMessageId: overrideParentMessageId || userMessageId,
text: text,
unfinished: true,
cancelled: false,
error: false
});
}
}
});
const abortController = new AbortController();
let response = await askBing({
text,
parentMessageId: userParentMessageId,
conversationId,
...endpointOption,
onProgress: progressCallback.call(null, {
res,
text,
parentMessageId: overrideParentMessageId || userMessageId
}),
abortController
});
console.log('BING RESPONSE', response);
const newConversationId = endpointOption?.jailbreak
? response.jailbreakConversationId
: response.conversationId || conversationId;
const newUserMassageId =
response.parentMessageId || response.details.requestId || userMessageId;
const newResponseMessageId = response.messageId || response.details.messageId;
// STEP1 generate response message
response.text =
response.response || response.details.spokenText || '**Bing refused to answer.**';
let responseMessage = {
conversationId: newConversationId,
messageId: responseMessageId,
newMessageId: newResponseMessageId,
parentMessageId: overrideParentMessageId || newUserMassageId,
sender: endpointOption?.jailbreak ? 'Sydney' : 'BingAI',
text: await handleText(response, true),
suggestions:
response.details.suggestedResponses &&
response.details.suggestedResponses.map((s) => s.text),
unfinished: false,
cancelled: false,
error: false
};
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
};
}
if (endpointOption?.jailbreak) {
conversationUpdate.jailbreak = true;
conversationUpdate.jailbreakConversationId = response.jailbreakConversationId;
} else {
conversationUpdate.jailbreak = false;
conversationUpdate.conversationSignature = response.conversationSignature;
conversationUpdate.clientId = response.clientId;
conversationUpdate.invocationId = response.invocationId;
}
await saveConvo(req.user.id, conversationUpdate);
conversationId = newConversationId;
// STEP3 update the user message
userMessage.conversationId = newConversationId;
userMessage.messageId = newUserMassageId;
// 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;
sendMessage(res, {
title: await getConvoTitle(req.user.id, conversationId),
final: true,
conversation: await getConvo(req.user.id, conversationId),
requestMessage: userMessage,
responseMessage: responseMessage
});
res.end();
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
const title = await titleConvo({
endpoint: endpointOption?.endpoint,
text,
response: responseMessage
});
await saveConvo(req.user.id, {
conversationId: conversationId,
title
});
}
} catch (error) {
console.log(error);
const errorMessage = {
messageId: responseMessageId,
sender: endpointOption?.jailbreak ? 'Sydney' : 'BingAI',
conversationId,
parentMessageId: overrideParentMessageId || userMessageId,
unfinished: false,
cancelled: false,
error: true,
text: error.message
};
await saveMessage(errorMessage);
handleError(res, errorMessage);
}
};
module.exports = router;

View File

@@ -0,0 +1,223 @@
const express = require('express');
const crypto = require('crypto');
const router = express.Router();
const { getChatGPTBrowserModels } = require('../endpoints');
const { browserClient } = require('../../../app/');
const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('../../../models');
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
const requireJwtAuth = require('../../../middleware/requireJwtAuth');
router.post('/', requireJwtAuth, async (req, res) => {
const {
endpoint,
text,
overrideParentMessageId = null,
parentMessageId,
conversationId: oldConversationId
} = req.body;
if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' });
if (endpoint !== 'chatGPTBrowser') return handleError(res, { text: 'Illegal request' });
// build user message
const conversationId = oldConversationId || crypto.randomUUID();
const isNewConversation = !oldConversationId;
const userMessageId = crypto.randomUUID();
const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000';
const userMessage = {
messageId: userMessageId,
sender: 'User',
text,
parentMessageId: userParentMessageId,
conversationId,
isCreatedByUser: true
};
// build endpoint option
const endpointOption = {
model: req.body?.model ?? 'text-davinci-002-render-sha',
token: req.body?.token ?? null
};
const availableModels = getChatGPTBrowserModels();
if (availableModels.find((model) => model === endpointOption.model) === undefined)
return handleError(res, { text: 'Illegal request: model' });
console.log('ask log', {
userMessage,
endpointOption,
conversationId
});
if (!overrideParentMessageId) {
await saveMessage(userMessage);
await saveConvo(req.user.id, {
...userMessage,
...endpointOption,
conversationId,
endpoint
});
}
// eslint-disable-next-line no-use-before-define
return await ask({
isNewConversation,
userMessage,
endpointOption,
conversationId,
preSendRequest: true,
overrideParentMessageId,
req,
res
});
});
const ask = async ({
isNewConversation,
userMessage,
endpointOption,
conversationId,
preSendRequest = true,
overrideParentMessageId = null,
req,
res
}) => {
let { text, parentMessageId: userParentMessageId, messageId: userMessageId } = userMessage;
const userId = req.user.id;
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'
});
if (preSendRequest) sendMessage(res, { message: userMessage, created: true });
let responseMessageId = crypto.randomUUID();
try {
let lastSavedTimestamp = 0;
const { onProgress: progressCallback, getPartialText } = createOnProgress({
onProgress: ({ text }) => {
const currentTimestamp = Date.now();
if (currentTimestamp - lastSavedTimestamp > 500) {
lastSavedTimestamp = currentTimestamp;
saveMessage({
messageId: responseMessageId,
sender: endpointOption?.jailbreak ? 'Sydney' : 'BingAI',
conversationId,
parentMessageId: overrideParentMessageId || userMessageId,
text: text,
unfinished: true,
cancelled: false,
error: false
});
}
}
});
const abortController = new AbortController();
let response = await browserClient({
text,
parentMessageId: userParentMessageId,
conversationId,
...endpointOption,
onProgress: progressCallback.call(null, { res, text }),
abortController,
userId
});
console.log('CLIENT RESPONSE', response);
const newConversationId = response.conversationId || conversationId;
const newUserMassageId = response.parentMessageId || userMessageId;
const newResponseMessageId = response.messageId;
// STEP1 generate response message
response.text = response.response || '**ChatGPT refused to answer.**';
let responseMessage = {
conversationId: newConversationId,
messageId: responseMessageId,
newMessageId: newResponseMessageId,
parentMessageId: overrideParentMessageId || newUserMassageId,
text: await handleText(response),
sender: endpointOption?.chatGptLabel || 'ChatGPT',
unfinished: false,
cancelled: false,
error: false
};
await saveMessage(responseMessage);
responseMessage.messageId = newResponseMessageId;
// STEP2 update the conversation
// First update conversationId if needed
let conversationUpdate = { conversationId: newConversationId, endpoint: 'chatGPTBrowser' };
if (conversationId != newConversationId)
if (isNewConversation) {
// change the conversationId to new one
conversationUpdate = {
...conversationUpdate,
conversationId: conversationId,
newConversationId: newConversationId
};
} else {
// create new conversation
conversationUpdate = {
...conversationUpdate,
...endpointOption
};
}
await saveConvo(req.user.id, conversationUpdate);
conversationId = newConversationId;
// STEP3 update the user message
userMessage.conversationId = newConversationId;
userMessage.messageId = newUserMassageId;
// 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;
sendMessage(res, {
title: await getConvoTitle(req.user.id, conversationId),
final: true,
conversation: await getConvo(req.user.id, conversationId),
requestMessage: userMessage,
responseMessage: responseMessage
});
res.end();
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
// const title = await titleConvo({ endpoint: endpointOption?.endpoint, text, response: responseMessage });
const title = await response.details.title;
await saveConvo(req.user.id, {
conversationId: conversationId,
title
});
}
} catch (error) {
const errorMessage = {
messageId: responseMessageId,
sender: 'ChatGPT',
conversationId,
parentMessageId: overrideParentMessageId || userMessageId,
unfinished: false,
cancelled: false,
error: true,
text: error.message
};
await saveMessage(errorMessage);
handleError(res, errorMessage);
}
};
module.exports = router;

View File

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

View File

@@ -0,0 +1,286 @@
const express = require('express');
const crypto = require('crypto');
const router = express.Router();
const addToCache = require('./addToCache');
const { getOpenAIModels } = require('../endpoints');
const { titleConvo, askClient } = require('../../../app/');
const { saveMessage, getConvoTitle, saveConvo, getConvo } = require('../../../models');
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
const requireJwtAuth = require('../../../middleware/requireJwtAuth');
const abortControllers = new Map();
router.post('/abort', requireJwtAuth, async (req, res) => {
const { abortKey } = req.body;
console.log(`req.body`, req.body);
if (!abortControllers.has(abortKey)) {
return res.status(404).send('Request not found');
}
const { abortController } = abortControllers.get(abortKey);
abortControllers.delete(abortKey);
const ret = await abortController.abortAsk();
console.log('Aborted request', abortKey);
console.log('Aborted message:', ret);
res.send(JSON.stringify(ret));
});
router.post('/', requireJwtAuth, async (req, res) => {
const {
endpoint,
text,
overrideParentMessageId = null,
parentMessageId,
conversationId: oldConversationId
} = req.body;
if (text.length === 0) return handleError(res, { text: 'Prompt empty or too short' });
if (endpoint !== 'openAI') return handleError(res, { text: 'Illegal request' });
// build user message
const conversationId = oldConversationId || crypto.randomUUID();
const isNewConversation = !oldConversationId;
const userMessageId = crypto.randomUUID();
const userParentMessageId = parentMessageId || '00000000-0000-0000-0000-000000000000';
const userMessage = {
messageId: userMessageId,
sender: 'User',
text,
parentMessageId: userParentMessageId,
conversationId,
isCreatedByUser: true
};
// build endpoint option
const endpointOption = {
model: req.body?.model ?? 'gpt-3.5-turbo',
chatGptLabel: req.body?.chatGptLabel ?? null,
promptPrefix: req.body?.promptPrefix ?? null,
temperature: req.body?.temperature ?? 1,
top_p: req.body?.top_p ?? 1,
presence_penalty: req.body?.presence_penalty ?? 0,
frequency_penalty: req.body?.frequency_penalty ?? 0
};
const availableModels = getOpenAIModels();
if (availableModels.find((model) => model === endpointOption.model) === undefined)
return handleError(res, { text: 'Illegal request: model' });
console.log('ask log', {
userMessage,
endpointOption,
conversationId
});
if (!overrideParentMessageId) {
await saveMessage(userMessage);
await saveConvo(req.user.id, {
...userMessage,
...endpointOption,
conversationId,
endpoint
});
}
// eslint-disable-next-line no-use-before-define
return await ask({
isNewConversation,
userMessage,
endpointOption,
conversationId,
preSendRequest: true,
overrideParentMessageId,
req,
res
});
});
const ask = async ({
isNewConversation,
userMessage,
endpointOption,
conversationId,
preSendRequest = true,
overrideParentMessageId = null,
req,
res
}) => {
let { text, parentMessageId: userParentMessageId, messageId: userMessageId } = userMessage;
const userId = req.user.id;
let responseMessageId = crypto.randomUUID();
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'
});
if (preSendRequest) sendMessage(res, { message: userMessage, created: true });
try {
let lastSavedTimestamp = 0;
const { onProgress: progressCallback, getPartialText } = createOnProgress({
onProgress: ({ text }) => {
const currentTimestamp = Date.now();
if (currentTimestamp - lastSavedTimestamp > 500) {
lastSavedTimestamp = currentTimestamp;
saveMessage({
messageId: responseMessageId,
sender: endpointOption?.chatGptLabel || 'ChatGPT',
conversationId,
parentMessageId: overrideParentMessageId || userMessageId,
text: text,
unfinished: true,
cancelled: false,
error: false
});
}
}
});
let abortController = new AbortController();
abortController.abortAsk = async function () {
this.abort();
const responseMessage = {
messageId: responseMessageId,
sender: endpointOption?.chatGptLabel || 'ChatGPT',
conversationId,
parentMessageId: overrideParentMessageId || userMessageId,
text: getPartialText(),
unfinished: false,
cancelled: true,
error: false
};
saveMessage(responseMessage);
await addToCache({ endpoint: 'openAI', endpointOption, userMessage, responseMessage });
return {
title: await getConvoTitle(req.user.id, conversationId),
final: true,
conversation: await getConvo(req.user.id, conversationId),
requestMessage: userMessage,
responseMessage: responseMessage
};
};
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,
text,
parentMessageId: overrideParentMessageId || userMessageId
}),
abortController,
userId
});
abortControllers.delete(abortKey);
console.log('CLIENT RESPONSE', response);
const newConversationId = response.conversationId || conversationId;
const newUserMassageId = response.parentMessageId || userMessageId;
const newResponseMessageId = response.messageId;
// STEP1 generate response message
response.text = response.response || '**ChatGPT refused to answer.**';
let responseMessage = {
conversationId: newConversationId,
messageId: responseMessageId,
newMessageId: newResponseMessageId,
parentMessageId: overrideParentMessageId || newUserMassageId,
text: await handleText(response),
sender: endpointOption?.chatGptLabel || 'ChatGPT',
unfinished: false,
cancelled: false,
error: false
};
await saveMessage(responseMessage);
responseMessage.messageId = newResponseMessageId;
// STEP2 update the conversation
let conversationUpdate = { conversationId: newConversationId, endpoint: 'openAI' };
if (conversationId != newConversationId)
if (isNewConversation) {
// change the conversationId to new one
conversationUpdate = {
...conversationUpdate,
conversationId: conversationId,
newConversationId: newConversationId
};
} else {
// create new conversation
conversationUpdate = {
...conversationUpdate,
...endpointOption
};
}
await saveConvo(req.user.id, conversationUpdate);
conversationId = newConversationId;
// STEP3 update the user message
userMessage.conversationId = newConversationId;
userMessage.messageId = newUserMassageId;
// 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;
sendMessage(res, {
title: await getConvoTitle(req.user.id, conversationId),
final: true,
conversation: await getConvo(req.user.id, conversationId),
requestMessage: userMessage,
responseMessage: responseMessage
});
res.end();
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
const title = await titleConvo({
endpoint: endpointOption?.endpoint,
text,
response: responseMessage,
oaiApiKey
});
await saveConvo(req.user.id, {
conversationId: conversationId,
title
});
}
} catch (error) {
console.error(error);
const errorMessage = {
messageId: responseMessageId,
sender: endpointOption?.chatGptLabel || 'ChatGPT',
conversationId,
parentMessageId: overrideParentMessageId || userMessageId,
unfinished: false,
cancelled: false,
error: true,
text: error.message
};
await saveMessage(errorMessage);
handleError(res, errorMessage);
}
};
module.exports = router;

View File

@@ -0,0 +1,102 @@
const _ = require('lodash');
const citationRegex = /\[\^\d+?\^]/g;
const backtick = /(?<!`)[`](?!`)/g;
// const singleBacktick = /(?<!`)[`](?!`)/;
const cursorDefault = '<span className="result-streaming">█</span>';
const { getCitations, citeText } = require('../../../app');
const handleError = (res, message) => {
res.write(`event: error\ndata: ${JSON.stringify(message)}\n\n`);
res.end();
};
const sendMessage = (res, message) => {
if (message.length === 0) {
return;
}
res.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`);
};
const createOnProgress = ({ onProgress: _onProgress }) => {
let i = 0;
let code = '';
let tokens = '';
let precode = '';
let blockCount = 0;
let codeBlock = false;
let cursor = cursorDefault;
const progressCallback = async (partial, { res, text, bing = false, ...rest }) => {
let chunk = partial === text ? '' : partial;
tokens += chunk;
precode += chunk;
tokens = tokens.replaceAll('[DONE]', '');
if (codeBlock) {
code += chunk;
}
if (precode.includes('```') && codeBlock) {
codeBlock = false;
cursor = cursorDefault;
precode = precode.replace(/```/g, '');
code = '';
}
if (precode.includes('```') && code === '') {
precode = precode.replace(/```/g, '');
codeBlock = true;
blockCount++;
cursor = blockCount > 1 ? '█\n\n```' : '█\n\n';
}
const backticks = precode.match(backtick);
if (backticks && !codeBlock && cursor === cursorDefault) {
precode = precode.replace(backtick, '');
cursor = '█';
}
if (tokens.match(/^\n/)) {
tokens = tokens.replace(/^\n/, '');
}
if (bing) {
tokens = citeText(tokens, true);
}
sendMessage(res, { text: tokens + cursor, message: true, initial: i === 0, ...rest });
_onProgress && _onProgress({ text: tokens, message: true, initial: i === 0, ...rest });
i++;
};
const onProgress = (opts) => {
return _.partialRight(progressCallback, opts);
};
const getPartialText = () => {
return tokens;
};
return { onProgress, getPartialText };
};
const handleText = async (response, bing = false) => {
let { text } = response;
// text = await detectCode(text);
response.text = text;
if (bing) {
// const hasCitations = response.response.match(citationRegex)?.length > 0;
const links = getCitations(response);
if (response.text.match(citationRegex)?.length > 0) {
text = citeText(response);
}
text += links?.length > 0 ? `\n<small>${links}</small>` : '';
}
return text;
};
module.exports = { handleError, sendMessage, createOnProgress, handleText };

View File

@@ -0,0 +1,15 @@
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);
module.exports = router;

View File

@@ -1,86 +0,0 @@
const express = require('express');
const crypto = require('crypto');
const router = express.Router();
const { titleConvo, getCitations, citeText, askBing } = require('../../app/');
const { saveMessage, deleteMessages, saveConvo } = require('../../models');
const { handleError, sendMessage } = require('./handlers');
const citationRegex = /\[\^\d+?\^]/g;
router.post('/', async (req, res) => {
const { model, text, ...convo } = req.body;
if (text.length === 0) {
return handleError(res, 'Prompt empty or too short');
}
const userMessageId = crypto.randomUUID();
let userMessage = { id: userMessageId, sender: 'User', text };
console.log('ask log', { model, ...userMessage, ...convo });
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'
});
try {
let tokens = '';
const progressCallback = async (partial) => {
tokens += partial === text ? '' : partial;
// tokens = appendCode(tokens);
tokens = citeText(tokens, true);
sendMessage(res, { text: tokens, message: true });
};
let response = await askBing({
text,
progressCallback,
convo
});
console.log('BING RESPONSE');
// console.dir(response, { depth: null });
const hasCitations = response.response.match(citationRegex)?.length > 0;
userMessage.conversationSignature =
convo.conversationSignature || response.conversationSignature;
userMessage.conversationId = convo.conversationId || response.conversationId;
userMessage.invocationId = response.invocationId;
await saveMessage(userMessage);
if (!convo.conversationSignature) {
response.title = await titleConvo({
model,
message: text,
response: JSON.stringify(response.response)
});
}
response.text = response.response;
delete response.response;
response.id = response.details.messageId;
response.suggestions =
response.details.suggestedResponses &&
response.details.suggestedResponses.map((s) => s.text);
response.sender = model;
response.final = true;
const links = getCitations(response);
response.text =
citeText(response) +
(links?.length > 0 && hasCitations ? `\n<small>${links}</small>` : '');
await saveMessage(response);
await saveConvo(response);
sendMessage(res, response);
res.end();
} catch (error) {
console.log(error);
await deleteMessages({ id: userMessageId });
handleError(res, error.message);
}
});
module.exports = router;

View File

@@ -1,96 +0,0 @@
const express = require('express');
const crypto = require('crypto');
const router = express.Router();
const { titleConvo, getCitations, citeText, askSydney } = require('../../app/');
const { saveMessage, deleteMessages, saveConvo, getConvoTitle } = require('../../models');
const { handleError, sendMessage } = require('./handlers');
const citationRegex = /\[\^\d+?\^]/g;
router.post('/', async (req, res) => {
const { model, text, ...convo } = req.body;
if (text.length === 0) {
return handleError(res, 'Prompt empty or too short');
}
const userMessageId = crypto.randomUUID();
let userMessage = { id: userMessageId, sender: 'User', text };
console.log('ask log', { model, ...userMessage, ...convo });
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'
});
try {
let tokens = '';
const progressCallback = async (partial) => {
tokens += partial === text ? '' : partial;
// tokens = appendCode(tokens);
tokens = citeText(tokens, true);
sendMessage(res, { text: tokens, message: true });
};
let response = await askSydney({
text,
progressCallback,
convo
});
console.log('SYDNEY RESPONSE');
console.log(response.response);
// console.dir(response, { depth: null });
const hasCitations = response.response.match(citationRegex)?.length > 0;
// Save sydney response
response.id = response.messageId;
// response.parentMessageId = convo.parentMessageId ? convo.parentMessageId : response.messageId;
response.parentMessageId = response.messageId;
response.invocationId = convo.invocationId ? convo.invocationId + 1 : 1;
response.title = convo.jailbreakConversationId
? await getConvoTitle(convo.conversationId)
: await titleConvo({
model,
message: text,
response: JSON.stringify(response.response)
});
response.conversationId = convo.conversationId
? convo.conversationId
: crypto.randomUUID();
response.conversationSignature = convo.conversationSignature
? convo.conversationSignature
: crypto.randomUUID();
response.text = response.response;
delete response.response;
response.suggestions =
response.details.suggestedResponses &&
response.details.suggestedResponses.map((s) => s.text);
response.sender = model;
response.final = true;
const links = getCitations(response);
response.text =
citeText(response) +
(links?.length > 0 && hasCitations ? `\n<small>${links}</small>` : '');
// Save user message
userMessage.conversationId = response.conversationId;
userMessage.parentMessageId = response.parentMessageId;
await saveMessage(userMessage);
// Save sydney response & convo, then send
await saveMessage(response);
await saveConvo(response);
sendMessage(res, response);
res.end();
} catch (error) {
console.log(error);
await deleteMessages({ id: userMessageId });
handleError(res, error.message);
}
});
module.exports = router;

25
api/server/routes/auth.js Normal file
View File

@@ -0,0 +1,25 @@
const express = require('express');
const {
resetPasswordRequestController,
resetPasswordController,
getUserController,
loginController,
logoutController,
refreshController,
registrationController
} = require('../controllers/auth.controller');
const requireJwtAuth = require('../../middleware/requireJwtAuth');
const requireLocalAuth = require('../../middleware/requireLocalAuth');
const router = express.Router();
//Local
router.get('/user', requireJwtAuth, getUserController);
router.post('/logout', requireJwtAuth, logoutController);
router.post('/login', requireLocalAuth, loginController);
router.post('/refresh', requireJwtAuth, refreshController);
router.post('/register', registrationController);
router.post('/requestPasswordReset', resetPasswordRequestController);
router.post('/resetPassword', resetPasswordController);
module.exports = router;

View File

@@ -1,21 +1,37 @@
const express = require('express');
const router = express.Router();
const { getConvos, deleteConvos, updateConvo } = require('../../models/Conversation');
const { getConvo, saveConvo } = require('../../models');
const { getConvosByPage, deleteConvos } = require('../../models/Conversation');
const requireJwtAuth = require('../../middleware/requireJwtAuth');
router.get('/', async (req, res) => {
router.get('/', requireJwtAuth, async (req, res) => {
const pageNumber = req.query.pageNumber || 1;
res.status(200).send(await getConvos(pageNumber));
res.status(200).send(await getConvosByPage(req.user.id, pageNumber));
});
router.post('/clear', async (req, res) => {
router.get('/:conversationId', requireJwtAuth, async (req, res) => {
const { conversationId } = req.params;
const convo = await getConvo(req.user.id, conversationId);
if (convo) res.status(200).send(convo.toObject());
else res.status(404).end();
});
router.post('/clear', requireJwtAuth, async (req, res) => {
let filter = {};
const { conversationId } = req.body.arg;
const { conversationId, source } = req.body.arg;
if (conversationId) {
filter = { conversationId };
}
console.log('source:', source);
if (source === 'button' && !conversationId) {
return res.status(200).send('No conversationId provided');
}
try {
const dbResponse = await deleteConvos(filter);
const dbResponse = await deleteConvos(req.user.id, filter);
res.status(201).send(dbResponse);
} catch (error) {
console.error(error);
@@ -23,11 +39,11 @@ router.post('/clear', async (req, res) => {
}
});
router.post('/update', async (req, res) => {
router.post('/update', requireJwtAuth, async (req, res) => {
const update = req.body.arg;
try {
const dbResponse = await updateConvo(update);
const dbResponse = await saveConvo(req.user.id, update);
res.status(201).send(dbResponse);
} catch (error) {
console.error(error);

View File

@@ -1,67 +0,0 @@
const express = require('express');
const router = express.Router();
const {
getCustomGpts,
updateCustomGpt,
updateByLabel,
deleteCustomGpts
} = require('../../models');
router.get('/', async (req, res) => {
const models = (await getCustomGpts()).map((model) => {
model = model.toObject();
model._id = model._id.toString();
return model;
});
res.status(200).send(models);
});
router.post('/delete', async (req, res) => {
const { arg } = req.body;
try {
await deleteCustomGpts(arg);
const models = (await getCustomGpts()).map((model) => {
model = model.toObject();
model._id = model._id.toString();
return model;
});
res.status(201).send(models);
// res.status(201).send(dbResponse);
} catch (error) {
console.error(error);
res.status(500).send(error);
}
});
// router.post('/create', async (req, res) => {
// const payload = req.body.arg;
// try {
// const dbResponse = await createCustomGpt(payload);
// res.status(201).send(dbResponse);
// } catch (error) {
// console.error(error);
// res.status(500).send(error);
// }
// });
router.post('/', async (req, res) => {
const update = req.body.arg;
let setter = updateCustomGpt;
if (update.prevLabel) {
setter = updateByLabel;
}
try {
const dbResponse = await setter(update);
res.status(201).send(dbResponse);
} catch (error) {
console.error(error);
res.status(500).send(error);
}
});
module.exports = router;

View File

@@ -0,0 +1,60 @@
const express = require('express');
const router = express.Router();
const getOpenAIModels = () => {
let models = ['gpt-4', 'text-davinci-003', 'gpt-3.5-turbo', 'gpt-3.5-turbo-0301'];
if (process.env.OPENAI_MODELS) models = String(process.env.OPENAI_MODELS).split(',');
return models;
};
const getChatGPTBrowserModels = () => {
let models = ['text-davinci-002-render-sha', 'gpt-4'];
if (process.env.CHATGPT_MODELS) models = String(process.env.CHATGPT_MODELS).split(',');
return models;
};
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 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;
const chatGPTBrowser = process.env.CHATGPT_TOKEN
? {
userProvide: process.env.CHATGPT_TOKEN == 'user_provided',
availableModels: getChatGPTBrowserModels()
}
: false;
res.send(JSON.stringify({ azureOpenAI, openAI, google, bingAI, chatGPTBrowser }));
});
module.exports = { router, getOpenAIModels, getChatGPTBrowserModels };

View File

@@ -1,13 +0,0 @@
const handleError = (res, errorMessage) => {
res.status(500).write(`event: error\ndata: ${errorMessage}`);
res.end();
};
const sendMessage = (res, message) => {
if (message.length === 0) {
return;
}
res.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`);
};
module.exports = { handleError, sendMessage };

View File

@@ -1,7 +1,23 @@
const ask = require('./ask');
const messages = require('./messages');
const convos = require('./convos');
const customGpts = require('./customGpts');
const presets = require('./presets');
const prompts = require('./prompts');
const search = require('./search');
const tokenizer = require('./tokenizer');
const auth = require('./auth');
const oauth = require('./oauth');
const { router: endpoints } = require('./endpoints');
module.exports = { ask, messages, convos, customGpts, prompts };
module.exports = {
search,
ask,
messages,
convos,
presets,
prompts,
auth,
oauth,
tokenizer,
endpoints
};

View File

@@ -1,8 +1,9 @@
const express = require('express');
const router = express.Router();
const { getMessages } = require('../../models/Message');
const requireJwtAuth = require('../../middleware/requireJwtAuth');
router.get('/:conversationId', async (req, res) => {
router.get('/:conversationId', requireJwtAuth, async (req, res) => {
const { conversationId } = req.params;
res.status(200).send(await getMessages({ conversationId }));
});

View File

@@ -0,0 +1,64 @@
const passport = require('passport');
const express = require('express');
const router = express.Router();
const isProduction = process.env.NODE_ENV === 'production';
const clientUrl = isProduction ? process.env.CLIENT_URL_PROD : process.env.CLIENT_URL_DEV;
// Social
router.get(
'/google',
passport.authenticate('google', {
scope: ['openid', 'profile', 'email'],
session: false
})
);
router.get(
'/google/callback',
passport.authenticate('google', {
failureRedirect: `${clientUrl}/login`,
failureMessage: true,
session: false,
scope: ['openid', 'profile', 'email']
}),
(req, res) => {
const token = req.user.generateToken();
res.cookie('token', token, {
expires: new Date(Date.now() + eval(process.env.SESSION_EXPIRY)),
httpOnly: false,
secure: isProduction
});
res.redirect(clientUrl);
}
);
router.get(
'/facebook',
passport.authenticate('facebook', {
scope: ['public_profile', 'email'],
session: false
})
);
router.get(
'/facebook/callback',
passport.authenticate('facebook', {
failureRedirect: `${clientUrl}/login`,
failureMessage: true,
session: false,
scope: ['public_profile', 'email']
}),
(req, res) => {
const token = req.user.generateToken();
res.cookie('token', token, {
expires: new Date(Date.now() + eval(process.env.SESSION_EXPIRY)),
httpOnly: false,
secure: isProduction
});
res.redirect(clientUrl);
}
);
module.exports = router;

View File

@@ -0,0 +1,54 @@
const express = require('express');
const router = express.Router();
const { getPresets, savePreset, deletePresets } = require('../../models');
const crypto = require('crypto');
const requireJwtAuth = require('../../middleware/requireJwtAuth');
router.get('/', requireJwtAuth, async (req, res) => {
const presets = (await getPresets(req.user.id)).map((preset) => {
return preset.toObject();
});
res.status(200).send(presets);
});
router.post('/', requireJwtAuth, async (req, res) => {
const update = req.body || {};
update.presetId = update?.presetId || crypto.randomUUID();
try {
await savePreset(req.user.id, update);
const presets = (await getPresets(req.user.id)).map((preset) => {
return preset.toObject();
});
res.status(201).send(presets);
} catch (error) {
console.error(error);
res.status(500).send(error);
}
});
router.post('/delete', requireJwtAuth, async (req, res) => {
let filter = {};
const { presetId } = req.body.arg || {};
if (presetId) filter = { presetId };
console.log('delete preset filter', filter);
try {
await deletePresets(req.user.id, filter);
const presets = (await getPresets(req.user.id)).map((preset) => preset.toObject());
// console.log('delete preset response', presets);
res.status(201).send(presets);
// res.status(201).send(dbResponse);
} catch (error) {
console.error(error);
res.status(500).send(error);
}
});
module.exports = router;

127
api/server/routes/search.js Normal file
View File

@@ -0,0 +1,127 @@
const express = require('express');
const router = express.Router();
const { MeiliSearch } = require('meilisearch');
const { Message } = require('../../models/Message');
const { Conversation, getConvosQueried } = require('../../models/Conversation');
const { reduceHits } = require('../../lib/utils/reduceHits');
const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc');
const requireJwtAuth = require('../../middleware/requireJwtAuth');
const cache = new Map();
router.get('/sync', async function (req, res) {
await Message.syncWithMeili();
await Conversation.syncWithMeili();
res.send('synced');
});
router.get('/', requireJwtAuth, async function (req, res) {
try {
let user = req.user.id;
user = user ?? null;
const { q } = req.query;
const pageNumber = req.query.pageNumber || 1;
const key = `${user || ''}${q}`;
if (cache.has(key)) {
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 });
return;
} else {
cache.clear();
}
// const message = await Message.meiliSearch(q);
const messages = (
await Message.meiliSearch(
q,
{
attributesToHighlight: ['text'],
highlightPreTag: '**',
highlightPostTag: '**'
},
true
)
).hits.map((message) => {
const { _formatted, ...rest } = message;
return {
...rest,
searchResult: true,
text: _formatted.text
};
});
const titles = (await Conversation.meiliSearch(q)).hits;
const sortedHits = reduceHits(messages, titles);
// debugging:
// console.log('user:', user, 'message hits:', messages.length, 'convo hits:', titles.length);
// console.log('sorted hits:', sortedHits.length);
const result = await getConvosQueried(user, sortedHits, pageNumber);
const activeMessages = [];
for (let i = 0; i < messages.length; i++) {
let message = messages[i];
if (message.conversationId.includes('--')) {
message.conversationId = cleanUpPrimaryKeyValue(message.conversationId);
}
if (result.convoMap[message.conversationId] && !message.error) {
const convo = result.convoMap[message.conversationId];
const { title, chatGptLabel, model } = convo;
message = { ...message, ...{ title, chatGptLabel, model } };
activeMessages.push(message);
}
}
result.messages = activeMessages;
if (result.cache) {
result.cache.messages = activeMessages;
cache.set(key, result.cache);
delete result.cache;
}
delete result.convoMap;
// for debugging
// console.log(result, messages.length);
res.status(200).send(result);
} catch (error) {
console.log(error);
res.status(500).send({ message: 'Error searching' });
}
});
router.get('/clear', async function (req, res) {
await Message.resetIndex();
res.send('cleared');
});
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 };
});
res.send(messages);
});
router.get('/enable', async function (req, res) {
let result = false;
try {
const client = new MeiliSearch({
host: process.env.MEILI_HOST,
apiKey: process.env.MEILI_MASTER_KEY
});
const { status } = await client.health();
// console.log(`Meilisearch: ${status}`);
result = status === 'available' && !!process.env.SEARCH;
return res.send(result);
} catch (error) {
// console.error(error);
return res.send(false);
}
});
module.exports = router;

View File

@@ -0,0 +1,26 @@
const express = require('express');
const router = express.Router();
const { Tiktoken } = require('@dqbd/tiktoken/lite');
const { load } = require('@dqbd/tiktoken/load');
const registry = require('@dqbd/tiktoken/registry.json');
const models = require('@dqbd/tiktoken/model_to_encoding.json');
const requireJwtAuth = require('../../middleware/requireJwtAuth');
router.post('/', requireJwtAuth, async (req, res) => {
try {
const { arg } = req.body;
// console.log('context:', arg, req.body);
// console.log(typeof req.body === 'object' ? { ...req.body, ...req.query } : req.query);
const model = await load(registry[models['gpt-3.5-turbo']]);
const encoder = new Tiktoken(model.bpe_ranks, model.special_tokens, model.pat_str);
const tokens = encoder.encode(arg.text);
encoder.free();
res.send({ count: tokens.length });
} catch (e) {
console.error(e);
res.status(500).send(e.message);
}
});
module.exports = router;

View File

@@ -0,0 +1,192 @@
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 DebugControl = require('../../utils/debug.js');
const { registerSchema } = require('../../strategies/validators');
const migrateDataToFirstUser = require('../../utils/migrateDataToFirstUser');
function log({ title, parameters }) {
DebugControl.log.functionName(title);
DebugControl.log.parameters(parameters);
}
const isProduction = process.env.NODE_ENV === 'production';
const clientUrl = isProduction ? process.env.CLIENT_URL_PROD : process.env.CLIENT_URL_DEV;
const loginUser = async (user) => {
// const refreshToken = req.user.generateRefreshToken();
const dbUser = await User.findById(user._id);
//todo: save refresh token
return dbUser;
};
const logoutUser = async (user, refreshToken) => {
try {
const userFound = await User.findById(user._id);
const tokenIndex = userFound.refreshToken.findIndex((item) => item.refreshToken === refreshToken);
if (tokenIndex !== -1) {
userFound.refreshToken.id(userFound.refreshToken[tokenIndex]._id).remove();
}
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 } = registerSchema.validate(user);
if (error) {
log({
title: 'Route: register - Joi Validation Error',
parameters: [
{ name: 'Request params:', value: user },
{ name: 'Validation error:', value: error.details }
]
});
response = { status: 422, message: error.details[0].message };
return response;
}
const { email, password, name, username } = user;
try {
const existingUser = await User.findOne({ email });
if (existingUser) {
log({
title: 'Register User - Email in use',
parameters: [
{ name: 'Request params:', value: user },
{ name: 'Existing user:', value: existingUser }
]
});
response = { status: 422, message: 'Email is in use' };
return response;
}
//determine if this is the first registered user (not counting anonymous_user)
const isFirstRegisteredUser = (await User.countDocuments({})) === 0;
try {
const newUser = await new User({
provider: 'local',
email,
password,
username,
name,
avatar: null,
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);
if (isFirstRegisteredUser) {
migrateDataToFirstUser(newUser);
// console.log(migrate);
}
response = { status: 200, user: newUser };
return response;
} catch (err) {
response = { status: 500, message: err.message };
return response;
}
} catch (err) {
response = { status: 500, message: err.message };
return response;
}
};
const requestPasswordReset = async (email) => {
const user = await User.findOne({ email });
if (!user) {
return new Error('Email does not exist');
}
let token = await Token.findOne({ userId: user._id });
if (token) await token.deleteOne();
let resetToken = crypto.randomBytes(32).toString('hex');
const hash = await bcrypt.hash(resetToken, 10);
await new Token({
userId: user._id,
token: hash,
createdAt: Date.now()
}).save();
const link = `${clientUrl}/reset-password?token=${resetToken}&userId=${user._id}`;
sendEmail(
user.email,
'Password Reset Request',
{
name: user.name,
link: link
},
'./template/requestResetPassword.handlebars'
);
return { link };
};
const resetPassword = async (userId, token, password) => {
let passwordResetToken = await Token.findOne({ userId });
if (!passwordResetToken) {
return new Error('Invalid or expired password reset token');
}
const isValid = await bcrypt.compare(token, passwordResetToken.token);
if (!isValid) {
return new Error('Invalid or expired password reset token');
}
const hash = await bcrypt.hash(password, 10);
await User.updateOne({ _id: userId }, { $set: { password: hash } }, { new: true });
const user = await User.findById({ _id: userId });
sendEmail(
user.email,
'Password Reset Successfnodeully',
{
name: user.name
},
'./template/resetPassword.handlebars'
);
await passwordResetToken.deleteOne();
return { message: 'Password reset was successful' };
};
module.exports = {
// signup,
registerUser,
loginUser,
logoutUser,
requestPasswordReset,
resetPassword
};

View File

@@ -0,0 +1,60 @@
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;
// facebook strategy
const facebookLogin = new FacebookStrategy(
{
clientID: process.env.FACEBOOK_APP_ID,
clientSecret: process.env.FACEBOOK_SECRET,
callbackURL: `${serverUrl}${process.env.FACEBOOK_CALLBACK_URL}`,
proxy: true
// profileFields: [
// 'id',
// 'email',
// 'gender',
// 'profileUrl',
// 'displayName',
// 'locale',
// 'name',
// 'timezone',
// 'updated_time',
// 'verified',
// 'picture.type(large)'
// ]
},
async (accessToken, refreshToken, profile, done) => {
console.log('facebookLogin => profile', profile);
try {
const oldUser = await User.findOne({ email: profile.emails[0].value });
if (oldUser) {
console.log('FACEBOOK LOGIN => found user', oldUser);
return done(null, oldUser);
}
} catch (err) {
console.log(err);
}
// register user
try {
const newUser = await new User({
provider: 'facebook',
facebookId: profile.id,
username: profile.name.givenName + profile.name.familyName,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0].value
}).save();
done(null, newUser);
} catch (err) {
console.log(err);
}
}
);
passport.use(facebookLogin);

View File

@@ -0,0 +1,44 @@
const passport = require('passport');
const { Strategy: GoogleStrategy } = require('passport-google-oauth20');
const User = require('../models/User');
const serverUrl =
process.env.NODE_ENV === 'production' ? process.env.SERVER_URL_PROD : process.env.SERVER_URL_DEV;
// google strategy
const googleLogin = new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: `${serverUrl}${process.env.GOOGLE_CALLBACK_URL}`,
proxy: true
},
async (accessToken, refreshToken, profile, cb) => {
try {
const oldUser = await User.findOne({ email: profile.emails[0].value });
if (oldUser) {
return cb(null, oldUser);
}
} catch (err) {
console.log(err);
}
try {
const newUser = await new User({
provider: 'google',
googleId: profile.id,
username: profile.name.givenName,
email: profile.emails[0].value,
emailVerified: profile.emails[0].verified,
name: `${profile.name.givenName} ${profile.name.familyName}`,
avatar: profile.photos[0].value
}).save();
cb(null, newUser);
} catch (err) {
console.log(err);
}
}
);
passport.use(googleLogin);

View File

@@ -0,0 +1,29 @@
const passport = require('passport');
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const User = require('../models/User');
const isProduction = process.env.NODE_ENV === 'production';
const secretOrKey = isProduction ? process.env.JWT_SECRET_PROD : process.env.JWT_SECRET_DEV;
// JWT strategy
const jwtLogin = new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey
},
async (payload, done) => {
try {
const user = await User.findById(payload.id);
if (user) {
done(null, user);
} else {
console.log('JwtStrategy => no user found');
done(null, false);
}
} catch (err) {
done(err, false);
}
}
);
passport.use(jwtLogin);

View File

@@ -0,0 +1,66 @@
const passport = require('passport');
const PassportLocalStrategy = require('passport-local').Strategy;
const User = require('../models/User');
const { loginSchema } = require('./validators');
const DebugControl = require('../utils/debug.js');
const passportLogin = new PassportLocalStrategy(
{
usernameField: 'email',
passwordField: 'password',
session: false,
passReqToCallback: true
},
async (req, email, password, done) => {
const { error } = loginSchema.validate(req.body);
if (error) {
log({
title: 'Passport Local Strategy - Validation Error',
parameters: [{ name: 'req.body', value: req.body }]
});
return done(null, false, { message: error.details[0].message });
}
try {
const user = await User.findOne({ email: email.trim() });
if (!user) {
log({
title: 'Passport Local Strategy - User Not Found',
parameters: [{ name: 'email', value: email }]
});
return done(null, false, { message: 'Email does not exists.' });
}
user.comparePassword(password, function (err, isMatch) {
if (err) {
log({
title: 'Passport Local Strategy - Compare password error',
parameters: [{ name: 'error', value: err }]
});
return done(err);
}
if (!isMatch) {
log({
title: 'Passport Local Strategy - Password does not match',
parameters: [{ name: 'isMatch', value: isMatch }]
});
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
});
} catch (err) {
return done(err);
}
}
);
passport.use(passportLogin);
function log({ title, parameters }) {
DebugControl.log.functionName(title);
if (parameters) {
DebugControl.log.parameters(parameters);
}
}

View File

@@ -0,0 +1,24 @@
const Joi = require('joi');
const loginSchema = Joi.object().keys({
email: Joi.string().trim().email().required(),
password: Joi.string().trim().min(6).max(20).required()
});
const registerSchema = Joi.object().keys({
name: Joi.string().trim().min(2).max(30).required(),
username: Joi.string()
.trim()
.min(2)
.max(20)
.regex(/^[a-zA-Z0-9_]+$/)
.required(),
email: Joi.string().trim().email().required(),
password: Joi.string().trim().min(6).max(20).required(),
confirm_password: Joi.string().trim().min(6).max(20).required()
});
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();
}
}
};

46
api/utils/debug.js Normal file
View File

@@ -0,0 +1,46 @@
const levels = {
NONE: 0,
LOW: 1,
MEDIUM: 2,
HIGH: 3
};
let level = levels.HIGH;
module.exports = {
levels,
setLevel: (l) => (level = l),
log: {
parameters: (parameters) => {
if (levels.HIGH > level) return;
console.group();
parameters.forEach((p) => console.log(`${p.name}:`, p.value));
console.groupEnd();
},
functionName: (name) => {
if (levels.MEDIUM > level) return;
console.log(`\nEXECUTING: ${name}\n`);
},
flow: (flow) => {
if (levels.LOW > level) return;
console.log(`\n\n\nBEGIN FLOW: ${flow}\n\n\n`);
},
variable: ({ name, value }) => {
if (levels.HIGH > level) return;
console.group();
console.group();
console.log(`VARIABLE ${name}:`, value);
console.groupEnd();
console.groupEnd();
},
request: () => (req, res, next) => {
if (levels.HIGH > level) return next();
console.log('Hit URL', req.url, 'with following:');
console.group();
console.log('Query:', req.query);
console.log('Body:', req.body);
console.groupEnd();
return next();
}
}
};

View File

@@ -0,0 +1,11 @@
<html>
<head>
<style>
</style>
</head>
<body>
<p>Hi {{name}},</p>
<p>Your password has been changed successfully.</p>
</body>
</html>

View File

@@ -0,0 +1,13 @@
<html>
<head>
<style>
</style>
</head>
<body>
<p>Hi {{name}},</p>
<h1>You have requested to reset your password.</h1>
<p> Please click the link below to reset your password.</p>
<a href="{{link}}">Reset Password</a>
</body>
</html>

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

@@ -0,0 +1,29 @@
const Conversation = require('../models/schema/convoSchema');
const Preset = require('../models/schema/presetSchema');
const migrateConversations = async (userId) => {
try {
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();
} 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;

54
api/utils/sendEmail.js Normal file
View File

@@ -0,0 +1,54 @@
const nodemailer = require('nodemailer');
const handlebars = require('handlebars');
const fs = require('fs');
const path = require('path');
const sendEmail = async (email, subject, payload, template) => {
try {
// create reusable transporter object using the default SMTP transport
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: 465,
auth: {
user: process.env.EMAIL_USERNAME,
pass: process.env.EMAIL_PASSWORD
}
});
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)
};
};
// Send email
transporter.sendMail(options(), (error, info) => {
if (error) {
return error;
} else {
return res.status(200).json({
success: true
});
}
});
} catch (error) {
return error;
}
};
/*
Example:
sendEmail(
"youremail@gmail.com,
"Email subject",
{ name: "Eze" },
"./templates/layouts/main.handlebars"
);
*/
module.exports = sendEmail;

View File

@@ -1,2 +0,0 @@
/node_modules
.env

23
client/.env.example Normal file
View File

@@ -0,0 +1,23 @@
###########################
# 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:
###########################
# The social login domain uses this to redirect to localhost:3080 when you run the app in dev mode with Vite.
# Use your domain name as the Prod URL when you deploy the app to a live domain.
# Please note that:
# Social login features will not work if you run the build version on port 3080 locally after modifying the Prod URL
VITE_SERVER_URL_DEV=http://localhost:3080
VITE_SERVER_URL_PROD=http://localhost:3080
# Enable Social Login
# This enables/disables the Login with Google button on the login page.
# Set to true if you have registered the app with google cloud services
# and have set the GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in the /api/.env file
VITE_SHOW_GOOGLE_LOGIN_OPTION=false

View File

@@ -1,22 +0,0 @@
# Stage 1
FROM node:19-alpine as builder
WORKDIR /client
# copy package.json into the container at /client
COPY package*.json /client/
# install dependencies
RUN npm install
# Copy the current directory contents into the container at /client
COPY . /client/
# Build webpack artifacts
RUN npm run build
# Stage 2
FROM nginx:stable-alpine
WORKDIR /usr/share/nginx/html
RUN rm -rf ./*
COPY --from=builder /client/public /usr/share/nginx/html
# Add your nginx.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf
ENTRYPOINT ["nginx", "-g", "daemon off;"]
# docker build -t react-client .

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