mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
Merge pull request #7 from danny-avila/easy-config-setup
dockerize app, improve ui/ux, add setup instructions
This commit is contained in:
commit
e5d25f45e3
131 changed files with 9315 additions and 7412 deletions
11
.gitignore
vendored
11
.gitignore
vendored
|
|
@ -1,6 +1,7 @@
|
||||||
### node etc ###
|
### node etc ###
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
|
data-node
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
|
@ -24,11 +25,16 @@ dist/
|
||||||
public/main.js
|
public/main.js
|
||||||
public/main.js.map
|
public/main.js.map
|
||||||
public/main.js.LICENSE.txt
|
public/main.js.LICENSE.txt
|
||||||
|
client/public/main.js
|
||||||
|
client/public/main.js.map
|
||||||
|
client/public/main.js.LICENSE.txt
|
||||||
|
|
||||||
# Dependency directorys
|
# Dependency directorys
|
||||||
# Deployed apps should consider commenting these lines out:
|
# Deployed apps should consider commenting these lines out:
|
||||||
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
|
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
|
||||||
node_modules/
|
node_modules/
|
||||||
|
api/node_modules/
|
||||||
|
client/node_modules/
|
||||||
bower_components/
|
bower_components/
|
||||||
|
|
||||||
# Floobits
|
# Floobits
|
||||||
|
|
@ -40,7 +46,10 @@ bower_components/
|
||||||
# Environment
|
# Environment
|
||||||
.env
|
.env
|
||||||
cache.json
|
cache.json
|
||||||
data/
|
api/data/
|
||||||
.eslintrc.js
|
.eslintrc.js
|
||||||
|
owner.yml
|
||||||
|
archive
|
||||||
|
.vscode/settings.json
|
||||||
|
|
||||||
src/style - official.css
|
src/style - official.css
|
||||||
13
.vscode/settings.json
vendored
13
.vscode/settings.json
vendored
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"editor.lightbulb.enabled": false,
|
|
||||||
"editor.parameterHints.enabled": false,
|
|
||||||
"editor.renderWhitespace": "all",
|
|
||||||
// "editor.snippetSuggestions": "none",
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.wordWrap": "on",
|
|
||||||
"emmet.showExpandedAbbreviation": "never",
|
|
||||||
"files.trimTrailingWhitespace": true,
|
|
||||||
"javascript.suggest.enabled": false,
|
|
||||||
"javascript.updateImportsOnFileMove.enabled": "never",
|
|
||||||
"javascript.validate.enable": true
|
|
||||||
}
|
|
||||||
169
README.md
169
README.md
|
|
@ -1,17 +1,26 @@
|
||||||
# ChatGPT Clone #
|
# ChatGPT Clone #
|
||||||

|

|
||||||
## Wrap all conversational AIs under one roof. ##
|
## All AI Conversations under One Roof. ##
|
||||||
Assistant AIs are the future and OpenAI revolutionized this movement with ChatGPT. While numerous methods exist to integrate these AIs, this app commemorates the original styling of ChatGPT, with the ability to integrate any current/future AI models through user-provided API keys, while improving upon original client features, such as conversation search and prompt templates. This project was built with the anticipation of the official ChatGPT API from OpenAI, and now uses it along with the free access method. Through this clone, you can avoid subscription-based models in favor of free or pay-per-call APIs. I will most likely not deploy this app, as it's mainly a learning experience, but feel free to clone or fork to create your own custom wrapper.
|
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).
|
||||||
|
|
||||||
|
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
|
## Updates
|
||||||
<details open>
|
<details open>
|
||||||
<summary><strong>2023-03-04</strong></summary>
|
<summary><strong>2023-03-07</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.
|
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>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>Previous Updates</strong></summary>
|
<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>
|
<details>
|
||||||
<summary><strong>2023-03-01</strong></summary>
|
<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.
|
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.
|
||||||
|
|
@ -35,48 +44,134 @@ Currently, this project is only functional with the `text-davinci-003` model.
|
||||||
</details>
|
</details>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
# Table of Contents
|
||||||
|
* [Roadmap](#roadmap)
|
||||||
|
* [Features](#features)
|
||||||
|
* [Tech Stack](#tech-stack)
|
||||||
|
* [Getting Started](#getting-started)
|
||||||
|
* [Prerequisites](#prerequisites)
|
||||||
|
* [Usage](#usage)
|
||||||
|
* [Local (npm)](#npm)
|
||||||
|
* [Docker](#docker)
|
||||||
|
* [Access Tokens](#access-tokens)
|
||||||
|
* [Updating](#updating)
|
||||||
|
* [Using a Reverse Proxy](#using-a-reverse-proxy)
|
||||||
|
* [Use Cases](#use-cases)
|
||||||
|
* [Origin](#origin)
|
||||||
|
* [Caveats](#caveats)
|
||||||
|
* [Contributing](#contributing)
|
||||||
|
* [License](#license)
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
> **Warning**
|
> **Warning**
|
||||||
|
|
||||||
> This is a work in progress. I'm building this in public. You can follow the progress here or on my [Linkedin](https://www.linkedin.com/in/danny-avila).
|
> 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 planned/recently finished features.
|
Here are my recently completed and planned features:
|
||||||
|
|
||||||
- [x] Rename, delete conversations
|
|
||||||
- [x] Persistent conversation
|
- [x] Persistent conversation
|
||||||
|
- [x] Rename, delete conversations
|
||||||
- [x] UI Error handling
|
- [x] UI Error handling
|
||||||
- [x] AI Model Selection
|
|
||||||
- [x] Bing AI integration
|
- [x] Bing AI integration
|
||||||
- [x] Remember last selected model
|
- [x] AI model change handling (start new convos within existing, remembers last selected)
|
||||||
- [x] Highlight.js for code blocks
|
- [x] Code block handling (highlighting, markdown, clipboard, language detection)
|
||||||
- [x] Markdown handling
|
- [x] Markdown handling
|
||||||
- [x] Language Detection for code blocks
|
|
||||||
- [x] 'Copy to clipboard' button for code blocks
|
|
||||||
- [x] Customize prompt prefix/label (custom ChatGPT using official API)
|
- [x] Customize prompt prefix/label (custom ChatGPT using official API)
|
||||||
- [x] AI model change handling (start new convos within existing convo)
|
|
||||||
- [x] Server convo pagination (limit fetch and load more with 'show more' button)
|
- [x] Server convo pagination (limit fetch and load more with 'show more' button)
|
||||||
- [ ] Config file for easy startup
|
- [x] Config file for easy startup (docker compose)
|
||||||
|
- [ ] Build test suite for CI/CD
|
||||||
- [ ] Conversation Search (by title)
|
- [ ] Conversation Search (by title)
|
||||||
- [ ] Resubmit/edit sent messages
|
- [ ] Resubmit/edit sent messages
|
||||||
- [ ] Semantic Search Option (requires more tokens)
|
- [ ] Semantic Search Option (requires more tokens)
|
||||||
- [ ] Bing AI Styling (for suggested responses, convo end, etc.)
|
- [ ] Bing AI Styling (for suggested responses, convo end, etc.)
|
||||||
- [ ] Prompt Templates/Search
|
- [ ] Prompt Templates/Search
|
||||||
- [ ] Refactor/clean up code (tech debt)
|
- [ ] Refactor/clean up code (tech debt)
|
||||||
|
- [ ] Optional use of local storage for credentials
|
||||||
- [ ] Mobile styling (half-finished)
|
- [ ] Mobile styling (half-finished)
|
||||||
|
- [ ] Deploy demo
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- Response streaming identical to ChatGPT
|
- Response streaming identical to ChatGPT through server-sent events
|
||||||
- UI from original ChatGPT, including Dark mode
|
- UI from original ChatGPT, including Dark mode
|
||||||
- AI model selection, including OpenAI's official ChatGPT API
|
- AI model selection (official ChatGPT API, BingAI, ChatGPT Free)
|
||||||
|
- Create and Save custom ChatGPTs*
|
||||||
|
|
||||||
|
^* ChatGPT can be 'customized' by setting a system message or prompt prefix and alternate 'role' to the API request
|
||||||
|
|
||||||
|
[More info here](https://platform.openai.com/docs/guides/chat/instructing-chat-models). Here's an [example from this app.]()
|
||||||
|
|
||||||
### Tech Stack
|
### Tech Stack
|
||||||
|
|
||||||
- Utilizes [node-chatgpt-api](https://github.com/waylaidwanderer/node-chatgpt-api)
|
- Utilizes [node-chatgpt-api](https://github.com/waylaidwanderer/node-chatgpt-api)
|
||||||
- Response streaming identical to ChatGPT through server-sent events
|
- No React boilerplate/toolchain/clone tutorials, created from scratch with react@latest
|
||||||
- Use of Tailwind CSS (like the official site) and [shadcn/ui](https://github.com/shadcn/ui) components
|
- Use of Tailwind CSS and [shadcn/ui](https://github.com/shadcn/ui) components
|
||||||
- highlight.js, useSWR, Redux, Express, MongoDB, [Keyv](https://www.npmjs.com/package/keyv)
|
- 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
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>ChatGPT Free Instructions</strong></summary>
|
||||||
|
|
||||||
|
|
||||||
|
**This has been disabled as is no longer working as of 3-07-23**
|
||||||
|
|
||||||
|
|
||||||
|
To get your Access token For ChatGPT 'Free Version', login to chat.openai.com, then visit https://chat.openai.com/api/auth/session.
|
||||||
|
|
||||||
|
|
||||||
|
**Warning:** There may be a high chance of your account being banned with this method. Continue doing so at your own risk.
|
||||||
|
|
||||||
|
</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.
|
||||||
|
|
||||||
|
**Note:** Specific error handling and styling for this model is still in progress.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Updating
|
||||||
|
- As the project is still a work-in-progress, you should pull the latest and run some of the steps above again
|
||||||
|
|
||||||
## Use Cases ##
|
## Use Cases ##
|
||||||
|
|
||||||
|
|
@ -84,26 +179,36 @@ Here are my planned/recently finished features.
|
||||||
- Using the official API, you'd have to generate 7.5 million words to expense the same cost as ChatGPT Plus ($20).
|
- 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
|
- ChatGPT/Google Bard/Bing AI conversations are lost in space or
|
||||||
cannot be searched past a certain timeframe.
|
cannot be searched past a certain timeframe.
|
||||||
- ChatGPT Free (at [chat.openai.com](https://chat.openai.com/chat)) is more limited than the API
|
- **Customize ChatGPT**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- ChatGPT Free is down.
|
- **API is not as limited as ChatGPT Free (at [chat.openai.com](https://chat.openai.com/chat))**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
- **ChatGPT Free is down.**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
## Origin ##
|
## 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.
|
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.
|
||||||
|
|
||||||
<!-- ## Solution ##
|
|
||||||
Serves and searches all conversations reliably. All AI convos under one house.
|
|
||||||
Pay per call and not per month (cents compared to dollars). -->
|
|
||||||
|
|
||||||
## How to Get Started ##
|
## Caveats
|
||||||
> **Warning**
|
### Regarding use of Official ChatGPT API
|
||||||
> Working on easy startup/config code. Still in development.
|
From [@waylaidwanderer](https://github.com/waylaidwanderer/node-chatgpt-api/blob/main/README.md#caveats):
|
||||||
|
|
||||||
<!-- ## License
|
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).
|
||||||
|
|
||||||
Licensed under the [insert license here](). -->
|
## 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.
|
||||||
|
|
|
||||||
2
api/.dockerignore
Normal file
2
api/.dockerignore
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
/node_modules
|
||||||
|
.env
|
||||||
7
api/.env.example
Normal file
7
api/.env.example
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
OPENAI_KEY=
|
||||||
|
PORT=3080
|
||||||
|
NODE_ENV=development
|
||||||
|
# Change this to your MongoDB URI if different and I recommend appending chatgpt-clone
|
||||||
|
MONGO_URI="mongodb://127.0.0.1:27017/chatgpt-clone"
|
||||||
|
CHATGPT_TOKEN=""
|
||||||
|
BING_TOKEN=""
|
||||||
14
api/DockerFile
Normal file
14
api/DockerFile
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
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
|
||||||
|
# Run the app when the container launches
|
||||||
|
CMD ["npm", "start"]
|
||||||
|
|
||||||
|
# docker build -t node-api .
|
||||||
54
api/app/detectCode.js
Normal file
54
api/app/detectCode.js
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
const { ModelOperations } = require('@vscode/vscode-languagedetection');
|
||||||
|
const codeRegex = /(```[\s\S]*?```)/g;
|
||||||
|
const languageMatch = /```(\w+)/;
|
||||||
|
|
||||||
|
const detectCode = async (text) => {
|
||||||
|
try {
|
||||||
|
if (!text.match(codeRegex)) {
|
||||||
|
// console.log('disqualified for non-code match')
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.match(languageMatch)) {
|
||||||
|
// console.log('disqualified for language match')
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('qualified for code match');
|
||||||
|
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);
|
||||||
|
const language = await modelOperations.runModel(code);
|
||||||
|
return part.replace(/^```/, `\`\`\`${language[0].languageId}`);
|
||||||
|
} else {
|
||||||
|
// return i > 0 ? '\n' + part : part;
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (await Promise.all(output)).join('');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Error in detectCode function\n', e);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// const example3 = {
|
||||||
|
// text: "By default, the function generates an 8-character password with uppercase and lowercase letters and digits, but no special characters.\n\nTo use this function, simply call it with the desired arguments. For example:\n\n```\n>>> generate_password()\n'wE5pUxV7'\n>>> generate_password(length=12, special_chars=True)\n'M4v&^gJ*8#qH'\n>>> generate_password(uppercase=False, digits=False)\n'zajyprxr'\n``` \n\nNote that the randomness is used to select characters from the available character sets, but the resulting password is always deterministic given the same inputs. This makes the function useful for generating secure passwords that meet specific requirements."
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const example4 = {
|
||||||
|
// text: 'here\'s a cool function:\n```\nimport random\nimport string\n\ndef generate_password(length=8, uppercase=True, lowercase=True, digits=True, special_chars=False):\n """Generate a random password with specified requirements.\n\n Args:\n length (int): The length of the password. Default is 8.\n uppercase (bool): Whether to include uppercase letters. Default is True.\n lowercase (bool): Whether to include lowercase letters. Default is True.\n digits (bool): Whether to include digits. Default is True.\n special_chars (bool): Whether to include special characters. Default is False.\n\n Returns:\n str: A random password with the specified requirements.\n """\n # Define character sets to use in password generation\n chars = ""\n if uppercase:\n chars += string.ascii_uppercase\n if lowercase:\n chars += string.ascii_lowercase\n if digits:\n chars += string.digits\n if special_chars:\n chars += string.punctuation\n\n # Generate the password\n password = "".join(random.choice(chars) for _ in range(length))\n return password\n```\n\nThis function takes several arguments'
|
||||||
|
// };
|
||||||
|
|
||||||
|
// write an immediately invoked function to test this
|
||||||
|
// (async () => {
|
||||||
|
// const result = await detectCode(example3.text);
|
||||||
|
// console.log(result);
|
||||||
|
// })();
|
||||||
|
|
||||||
|
module.exports = detectCode;
|
||||||
|
|
@ -75,17 +75,30 @@ module.exports = {
|
||||||
},
|
},
|
||||||
// getConvos: async () => await Conversation.find({}).sort({ created: -1 }).exec(),
|
// getConvos: async () => await Conversation.find({}).sort({ created: -1 }).exec(),
|
||||||
getConvos: async (pageNumber = 1, pageSize = 12) => {
|
getConvos: async (pageNumber = 1, pageSize = 12) => {
|
||||||
const skip = (pageNumber - 1) * pageSize;
|
try {
|
||||||
// const limit = pageNumber * pageSize;
|
const skip = (pageNumber - 1) * pageSize;
|
||||||
|
// const limit = pageNumber * pageSize;
|
||||||
|
|
||||||
const conversations = await Conversation.find({})
|
const conversations = await Conversation.find({})
|
||||||
.sort({ created: -1 })
|
.sort({ created: -1 })
|
||||||
.skip(skip)
|
.skip(skip)
|
||||||
// .limit(limit)
|
// .limit(limit)
|
||||||
.limit(pageSize)
|
.limit(pageSize)
|
||||||
.exec();
|
.exec();
|
||||||
|
|
||||||
return conversations;
|
return conversations;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return { message: 'Error getting conversations' };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getConvo: async (conversationId) => {
|
||||||
|
try {
|
||||||
|
return await Conversation.findOne({ conversationId }).exec();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return { message: 'Error getting single conversation' };
|
||||||
|
}
|
||||||
},
|
},
|
||||||
deleteConvos: async (filter) => {
|
deleteConvos: async (filter) => {
|
||||||
let deleteCount = await Conversation.deleteMany(filter).exec();
|
let deleteCount = await Conversation.deleteMany(filter).exec();
|
||||||
|
|
@ -45,8 +45,6 @@ module.exports = {
|
||||||
},
|
},
|
||||||
updateCustomGpt: async ({ value, ...update }) => {
|
updateCustomGpt: async ({ value, ...update }) => {
|
||||||
try {
|
try {
|
||||||
console.log('updateCustomGpt', value, update);
|
|
||||||
|
|
||||||
const customGpt = await CustomGpt.findOne({ value }).exec();
|
const customGpt = await CustomGpt.findOne({ value }).exec();
|
||||||
|
|
||||||
if (!customGpt) {
|
if (!customGpt) {
|
||||||
|
|
@ -62,6 +60,17 @@ module.exports = {
|
||||||
return { message: 'Error updating customGpt' };
|
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) => {
|
deleteCustomGpts: async (filter) => {
|
||||||
try {
|
try {
|
||||||
return await CustomGpt.deleteMany(filter).exec();
|
return await CustomGpt.deleteMany(filter).exec();
|
||||||
14
api/models/index.js
Normal file
14
api/models/index.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
const { saveMessage, deleteMessages } = require('./Message');
|
||||||
|
const { getCustomGpts, updateCustomGpt, updateByLabel, deleteCustomGpts } = require('./CustomGpt');
|
||||||
|
const { getConvo, saveConvo } = require('./Conversation');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
saveMessage,
|
||||||
|
deleteMessages,
|
||||||
|
getConvo,
|
||||||
|
saveConvo,
|
||||||
|
getCustomGpts,
|
||||||
|
updateCustomGpt,
|
||||||
|
updateByLabel,
|
||||||
|
deleteCustomGpts
|
||||||
|
};
|
||||||
6
api/nodemon.json
Normal file
6
api/nodemon.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"ignore": [
|
||||||
|
"api/data/",
|
||||||
|
"data"
|
||||||
|
]
|
||||||
|
}
|
||||||
7893
api/package-lock.json
generated
Normal file
7893
api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
37
api/package.json
Normal file
37
api/package.json
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"name": "chatgpt-clone",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "server/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server/index.js",
|
||||||
|
"server-dev": "npx nodemon server/index.js"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/danny-avila/chatgpt-clone.git"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/danny-avila/chatgpt-clone/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/danny-avila/chatgpt-clone#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"@keyv/mongo": "^2.1.8",
|
||||||
|
"@vscode/vscode-languagedetection": "^1.0.22",
|
||||||
|
"@waylaidwanderer/chatgpt-api": "^1.15.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"keyv": "^4.5.2",
|
||||||
|
"keyv-file": "^0.2.0",
|
||||||
|
"mongoose": "^6.9.0",
|
||||||
|
"openai": "^3.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^2.0.20",
|
||||||
|
"path": "^0.12.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,8 @@ const path = require('path');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const routes = require('./routes');
|
const routes = require('./routes');
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 3050;
|
const port = process.env.PORT || 3080;
|
||||||
const projectPath = path.join(__dirname, '..');
|
const projectPath = path.join(__dirname, '..', '..', 'client');
|
||||||
dbConnect().then(() => console.log('Connected to MongoDB'));
|
dbConnect().then(() => console.log('Connected to MongoDB'));
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
|
@ -17,11 +17,11 @@ app.get('/', function (req, res) {
|
||||||
res.sendFile(path.join(projectPath, 'public', 'index.html'));
|
res.sendFile(path.join(projectPath, 'public', 'index.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use('/ask', routes.ask);
|
app.use('/api/ask', routes.ask);
|
||||||
app.use('/messages', routes.messages);
|
app.use('/api/messages', routes.messages);
|
||||||
app.use('/convos', routes.convos);
|
app.use('/api/convos', routes.convos);
|
||||||
app.use('/customGpts', routes.customGpts);
|
app.use('/api/customGpts', routes.customGpts);
|
||||||
app.use('/prompts', routes.prompts);
|
app.use('/api/prompts', routes.prompts);
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Server listening at http://localhost:${port}`);
|
console.log(`Server listening at http://localhost:${port}`);
|
||||||
|
|
@ -9,14 +9,13 @@ const {
|
||||||
customClient,
|
customClient,
|
||||||
detectCode
|
detectCode
|
||||||
} = require('../../app/');
|
} = require('../../app/');
|
||||||
const { saveMessage, deleteMessages, saveConvo } = require('../../models');
|
const { getConvo, saveMessage, deleteMessages, saveConvo } = require('../../models');
|
||||||
const { handleError, sendMessage } = require('./handlers');
|
const { handleError, sendMessage } = require('./handlers');
|
||||||
|
|
||||||
router.use('/bing', askBing);
|
router.use('/bing', askBing);
|
||||||
|
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
const { model, text, parentMessageId, conversationId, chatGptLabel, promptPrefix } =
|
let { model, text, parentMessageId, conversationId, chatGptLabel, promptPrefix } = req.body;
|
||||||
req.body;
|
|
||||||
if (!text.trim().includes(' ') && text.length < 5) {
|
if (!text.trim().includes(' ') && text.length < 5) {
|
||||||
return handleError(res, 'Prompt empty or too short');
|
return handleError(res, 'Prompt empty or too short');
|
||||||
}
|
}
|
||||||
|
|
@ -43,6 +42,15 @@ router.post('/', async (req, res) => {
|
||||||
client = browserClient;
|
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, {
|
res.writeHead(200, {
|
||||||
Connection: 'keep-alive',
|
Connection: 'keep-alive',
|
||||||
'Content-Type': 'text/event-stream',
|
'Content-Type': 'text/event-stream',
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { getCustomGpts, updateCustomGpt, deleteCustomGpts } = require('../../models');
|
const { getCustomGpts, updateCustomGpt, updateByLabel, deleteCustomGpts } = require('../../models');
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
const models = (await getCustomGpts()).map(model => {
|
const models = (await getCustomGpts()).map(model => {
|
||||||
|
|
@ -8,20 +8,14 @@ router.get('/', async (req, res) => {
|
||||||
model._id = model._id.toString();
|
model._id = model._id.toString();
|
||||||
return model;
|
return model;
|
||||||
});
|
});
|
||||||
// console.log(models);
|
|
||||||
res.status(200).send(models);
|
res.status(200).send(models);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/delete/:_id', async (req, res) => {
|
router.post('/delete', async (req, res) => {
|
||||||
const { _id } = req.params;
|
const { arg } = req.body;
|
||||||
let filter = {};
|
|
||||||
|
|
||||||
if (_id) {
|
|
||||||
filter = { _id };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dbResponse = await deleteCustomGpts(filter);
|
const dbResponse = await deleteCustomGpts(arg);
|
||||||
res.status(201).send(dbResponse);
|
res.status(201).send(dbResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
@ -44,8 +38,14 @@ router.post('/delete/:_id', async (req, res) => {
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
const update = req.body.arg;
|
const update = req.body.arg;
|
||||||
|
|
||||||
|
let setter = updateCustomGpt;
|
||||||
|
|
||||||
|
if (update.prevLabel) {
|
||||||
|
setter = updateByLabel;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dbResponse = await updateCustomGpt(update);
|
const dbResponse = await setter(update);
|
||||||
res.status(201).send(dbResponse);
|
res.status(201).send(dbResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
const { ModelOperations } = require('@vscode/vscode-languagedetection');
|
|
||||||
const codeRegex = /(```[\s\S]*?```)/g;
|
|
||||||
const languageMatch = /```(\w+)/;
|
|
||||||
|
|
||||||
const detectCode = async (text) => {
|
|
||||||
try {
|
|
||||||
if (!text.match(codeRegex)) {
|
|
||||||
// console.log('disqualified for non-code match')
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text.match(languageMatch)) {
|
|
||||||
// console.log('disqualified for language match')
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log('qualified for code match');
|
|
||||||
const modelOperations = new ModelOperations();
|
|
||||||
const regexSplit = (await import('../src/utils/regexSplit.mjs')).default;
|
|
||||||
const parts = regexSplit(text, codeRegex);
|
|
||||||
|
|
||||||
const output = parts.map(async (part, i) => {
|
|
||||||
if (part.match(codeRegex)) {
|
|
||||||
const code = part.slice(3, -3);
|
|
||||||
const language = await modelOperations.runModel(code);
|
|
||||||
return part.replace(/^```/, `\`\`\`${language[0].languageId}`);
|
|
||||||
} else {
|
|
||||||
// return i > 0 ? '\n' + part : part;
|
|
||||||
return part;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (await Promise.all(output)).join('');
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Error in detectCode function\n', e);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const example3 = {
|
|
||||||
text: "By default, the function generates an 8-character password with uppercase and lowercase letters and digits, but no special characters.\n\nTo use this function, simply call it with the desired arguments. For example:\n\n```\n>>> generate_password()\n'wE5pUxV7'\n>>> generate_password(length=12, special_chars=True)\n'M4v&^gJ*8#qH'\n>>> generate_password(uppercase=False, digits=False)\n'zajyprxr'\n``` \n\nNote that the randomness is used to select characters from the available character sets, but the resulting password is always deterministic given the same inputs. This makes the function useful for generating secure passwords that meet specific requirements."
|
|
||||||
};
|
|
||||||
|
|
||||||
const example4 = {
|
|
||||||
text: 'here\'s a cool function:\n```\nimport random\nimport string\n\ndef generate_password(length=8, uppercase=True, lowercase=True, digits=True, special_chars=False):\n """Generate a random password with specified requirements.\n\n Args:\n length (int): The length of the password. Default is 8.\n uppercase (bool): Whether to include uppercase letters. Default is True.\n lowercase (bool): Whether to include lowercase letters. Default is True.\n digits (bool): Whether to include digits. Default is True.\n special_chars (bool): Whether to include special characters. Default is False.\n\n Returns:\n str: A random password with the specified requirements.\n """\n # Define character sets to use in password generation\n chars = ""\n if uppercase:\n chars += string.ascii_uppercase\n if lowercase:\n chars += string.ascii_lowercase\n if digits:\n chars += string.digits\n if special_chars:\n chars += string.punctuation\n\n # Generate the password\n password = "".join(random.choice(chars) for _ in range(length))\n return password\n```\n\nThis function takes several arguments'
|
|
||||||
};
|
|
||||||
|
|
||||||
// write an immediately invoked function to test this
|
|
||||||
// (async () => {
|
|
||||||
// const result = await detectCode(example3.text);
|
|
||||||
// console.log(result);
|
|
||||||
// })();
|
|
||||||
|
|
||||||
module.exports = detectCode;
|
|
||||||
2
client/.dockerignore
Normal file
2
client/.dockerignore
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
/node_modules
|
||||||
|
.env
|
||||||
22
client/Dockerfile
Normal file
22
client/Dockerfile
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# 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/
|
||||||
|
# Run the app when the container launches
|
||||||
|
CMD ["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 .
|
||||||
15
client/nginx.conf
Normal file
15
client/nginx.conf
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# Serve your React app
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
# Proxy requests to the API service
|
||||||
|
proxy_pass http://api:3080/api;
|
||||||
|
}
|
||||||
|
}
|
||||||
7833
package-lock.json → client/package-lock.json
generated
7833
package-lock.json → client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,56 +1,42 @@
|
||||||
{
|
{
|
||||||
"name": "rpp2210-mvp",
|
"name": "chatgpt-clone",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack-dev-server .",
|
"build": "webpack",
|
||||||
"build": "Webpack .",
|
"build-dev": "Webpack . --watch"
|
||||||
"server": "npx node server/index.js",
|
|
||||||
"build-dev": "Webpack . --watch",
|
|
||||||
"server-dev": "npx nodemon server/index.js",
|
|
||||||
"test": "test"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/danny-avila/rpp2210-mvp.git"
|
"url": "git+https://github.com/danny-avila/chatgpt-clone.git"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/danny-avila/rpp2210-mvp/issues"
|
"url": "https://github.com/danny-avila/chatgpt-clone/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/danny-avila/rpp2210-mvp#readme",
|
"homepage": "https://github.com/danny-avila/chatgpt-clone#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@keyv/mongo": "^2.1.8",
|
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.2",
|
"@radix-ui/react-alert-dialog": "^1.0.2",
|
||||||
"@radix-ui/react-dialog": "^1.0.2",
|
"@radix-ui/react-dialog": "^1.0.2",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.2",
|
"@radix-ui/react-dropdown-menu": "^2.0.2",
|
||||||
"@radix-ui/react-label": "^2.0.0",
|
"@radix-ui/react-label": "^2.0.0",
|
||||||
"@radix-ui/react-tabs": "^1.0.2",
|
"@radix-ui/react-tabs": "^1.0.2",
|
||||||
"@reduxjs/toolkit": "^1.9.2",
|
"@reduxjs/toolkit": "^1.9.2",
|
||||||
"@vscode/vscode-languagedetection": "^1.0.22",
|
"axios": "^1.3.4",
|
||||||
"@waylaidwanderer/chatgpt-api": "^1.15.1",
|
|
||||||
"chatgpt": "^4.2.0",
|
|
||||||
"class-variance-authority": "^0.4.0",
|
"class-variance-authority": "^0.4.0",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"cors": "^2.8.5",
|
|
||||||
"crypto-browserify": "^3.12.0",
|
"crypto-browserify": "^3.12.0",
|
||||||
"dotenv": "^16.0.3",
|
|
||||||
"highlight.js": "^11.7.0",
|
"highlight.js": "^11.7.0",
|
||||||
"keyv": "^4.5.2",
|
|
||||||
"keyv-file": "^0.2.0",
|
|
||||||
"lucide-react": "^0.113.0",
|
"lucide-react": "^0.113.0",
|
||||||
"markdown-to-jsx": "^7.1.9",
|
"markdown-to-jsx": "^7.1.9",
|
||||||
"mongoose": "^6.9.0",
|
|
||||||
"openai": "^3.1.0",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-redux": "^8.0.5",
|
"react-redux": "^8.0.5",
|
||||||
"react-textarea-autosize": "^8.4.0",
|
"react-textarea-autosize": "^8.4.0",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"remark-supersub": "^1.0.0",
|
|
||||||
"swr": "^2.0.3",
|
"swr": "^2.0.3",
|
||||||
"tailwind-merge": "^1.9.1",
|
"tailwind-merge": "^1.9.1",
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"tailwindcss-animate": "^1.0.5",
|
||||||
|
|
@ -75,8 +61,6 @@
|
||||||
"eslint-plugin-jest": "^27.2.1",
|
"eslint-plugin-jest": "^27.2.1",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
|
||||||
"nodemon": "^2.0.20",
|
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"postcss-loader": "^7.0.2",
|
"postcss-loader": "^7.0.2",
|
||||||
|
Before Width: | Height: | Size: 828 B After Width: | Height: | Size: 828 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
|
@ -16,15 +16,15 @@ export default function Conversation({
|
||||||
title = 'New conversation',
|
title = 'New conversation',
|
||||||
bingData,
|
bingData,
|
||||||
chatGptLabel = null,
|
chatGptLabel = null,
|
||||||
promptPrefix = null
|
promptPrefix = null,
|
||||||
}) {
|
}) {
|
||||||
const [renaming, setRenaming] = useState(false);
|
const [renaming, setRenaming] = useState(false);
|
||||||
const [titleInput, setTitleInput] = useState(title);
|
const [titleInput, setTitleInput] = useState(title);
|
||||||
const { modelMap } = useSelector((state) => state.models);
|
const { modelMap } = useSelector((state) => state.models);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { trigger } = manualSWR(`http://localhost:3050/messages/${id}`, 'get');
|
const { trigger } = manualSWR(`http://localhost:3080/api/messages/${id}`, 'get');
|
||||||
const rename = manualSWR(`http://localhost:3050/convos/update`, 'post');
|
const rename = manualSWR(`http://localhost:3080/api/convos/update`, 'post');
|
||||||
|
|
||||||
const clickHandler = async () => {
|
const clickHandler = async () => {
|
||||||
if (conversationId === id) {
|
if (conversationId === id) {
|
||||||
|
|
@ -97,7 +97,7 @@ export default function Conversation({
|
||||||
rename.trigger({ conversationId, title: titleInput });
|
rename.trigger({ conversationId, title: titleInput });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyPress = (e) => {
|
const handleKeyDown = (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
onRename(e);
|
onRename(e);
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +128,7 @@ export default function Conversation({
|
||||||
value={titleInput}
|
value={titleInput}
|
||||||
onChange={(e) => setTitleInput(e.target.value)}
|
onChange={(e) => setTitleInput(e.target.value)}
|
||||||
onBlur={onRename}
|
onBlur={onRename}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
titleInput
|
titleInput
|
||||||
|
|
@ -3,16 +3,17 @@ import TrashIcon from '../svg/TrashIcon';
|
||||||
import CrossIcon from '../svg/CrossIcon';
|
import CrossIcon from '../svg/CrossIcon';
|
||||||
import manualSWR from '~/utils/fetchers';
|
import manualSWR from '~/utils/fetchers';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { setConversation } from '~/store/convoSlice';
|
import { setConversation, removeConvo } from '~/store/convoSlice';
|
||||||
import { setMessages } from '~/store/messageSlice';
|
import { setMessages } from '~/store/messageSlice';
|
||||||
|
|
||||||
export default function DeleteButton({ conversationId, renaming, cancelHandler }) {
|
export default function DeleteButton({ conversationId, renaming, cancelHandler }) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { trigger } = manualSWR(
|
const { trigger } = manualSWR(
|
||||||
'http://localhost:3050/convos/clear',
|
`http://localhost:3080/api/convos/clear`,
|
||||||
'post',
|
'post',
|
||||||
() => {
|
() => {
|
||||||
dispatch(setMessages([]));
|
dispatch(setMessages([]));
|
||||||
|
dispatch(removeConvo(conversationId));
|
||||||
dispatch(setConversation({ title: 'New chat', conversationId: null, parentMessageId: null }));
|
dispatch(setConversation({ title: 'New chat', conversationId: null, parentMessageId: null }));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -2,11 +2,14 @@ import React from 'react';
|
||||||
import RenameIcon from '../svg/RenameIcon';
|
import RenameIcon from '../svg/RenameIcon';
|
||||||
import CheckMark from '../svg/CheckMark';
|
import CheckMark from '../svg/CheckMark';
|
||||||
|
|
||||||
export default function RenameButton({ onClick, renaming, renameHandler, onRename }) {
|
export default function RenameButton({ renaming, renameHandler, onRename, twcss }) {
|
||||||
const handler = renaming ? onRename : renameHandler;
|
const handler = renaming ? onRename : renameHandler;
|
||||||
|
const classProp = { className: "p-1 hover:text-white" };
|
||||||
|
if (twcss) {
|
||||||
|
classProp.className = twcss;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<button className="p-1 hover:text-white" onClick={handler}>
|
<button {...classProp} onClick={handler}>
|
||||||
{renaming ? <CheckMark /> : <RenameIcon />}
|
{renaming ? <CheckMark /> : <RenameIcon />}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
@ -56,16 +56,21 @@ export default function Message({
|
||||||
|
|
||||||
if (notUser) {
|
if (notUser) {
|
||||||
props.className =
|
props.className =
|
||||||
'w-full border-b border-black/10 bg-gray-50 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group bg-gray-100 dark:bg-[#444654]';
|
'w-full border-b border-black/10 bg-gray-50 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group bg-gray-100 dark:bg-[#444654]';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notUser && backgroundColor || sender === 'bingai') {
|
if ((notUser && backgroundColor) || sender === 'bingai') {
|
||||||
icon = (
|
icon = (
|
||||||
<div
|
<div
|
||||||
style={{ backgroundColor }}
|
style={{ backgroundColor }}
|
||||||
className="relative flex h-[30px] w-[30px] items-center justify-center rounded-sm p-1 text-white"
|
className="relative flex h-[30px] w-[30px] items-center justify-center rounded-sm p-1 text-white"
|
||||||
>
|
>
|
||||||
{sender === 'bingai' ? <BingIcon /> : <GPTIcon />}
|
{sender === 'bingai' ? <BingIcon /> : <GPTIcon />}
|
||||||
|
{error && (
|
||||||
|
<span className="absolute right-0 top-[20px] -mr-2 flex h-4 w-4 items-center justify-center rounded-full border border-white bg-red-500 text-[10px] text-white">
|
||||||
|
!
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ModelItem from './ModelItem';
|
import ModelItem from './ModelItem';
|
||||||
|
|
||||||
export default function MenuItems({ models }) {
|
export default function MenuItems({ models, onSelect }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{models.map((modelItem, i) => (
|
{models.map((modelItem, i) => (
|
||||||
|
|
@ -9,6 +9,7 @@ export default function MenuItems({ models }) {
|
||||||
key={i}
|
key={i}
|
||||||
modelName={modelItem.name}
|
modelName={modelItem.name}
|
||||||
value={modelItem.value}
|
value={modelItem.value}
|
||||||
|
onSelect={onSelect}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { setModel, setCustomGpt } from '~/store/submitSlice';
|
import { setModel, setCustomGpt } from '~/store/submitSlice';
|
||||||
import manualSWR from '~/utils/fetchers';
|
import manualSWR from '~/utils/fetchers';
|
||||||
import { Button } from '../ui/Button.tsx';
|
import { Button } from '../ui/Button.tsx';
|
||||||
|
|
@ -16,14 +16,15 @@ import {
|
||||||
DialogTitle
|
DialogTitle
|
||||||
} from '../ui/Dialog.tsx';
|
} from '../ui/Dialog.tsx';
|
||||||
|
|
||||||
export default function ModelDialog({ mutate, modelMap }) {
|
export default function ModelDialog({ mutate, setModelSave, handleSaveState }) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const { modelMap, initial } = useSelector((state) => state.models);
|
||||||
const [chatGptLabel, setChatGptLabel] = useState('');
|
const [chatGptLabel, setChatGptLabel] = useState('');
|
||||||
const [promptPrefix, setPromptPrefix] = useState('');
|
const [promptPrefix, setPromptPrefix] = useState('');
|
||||||
const [saveText, setSaveText] = useState('Save');
|
const [saveText, setSaveText] = useState('Save');
|
||||||
const [required, setRequired] = useState(false);
|
const [required, setRequired] = useState(false);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
const updateCustomGpt = manualSWR('http://localhost:3050/customGpts/', 'post');
|
const updateCustomGpt = manualSWR(`http://localhost:3080/api/customGpts/`, 'post');
|
||||||
|
|
||||||
const submitHandler = (e) => {
|
const submitHandler = (e) => {
|
||||||
if (chatGptLabel.length === 0) {
|
if (chatGptLabel.length === 0) {
|
||||||
|
|
@ -34,11 +35,13 @@ export default function ModelDialog({ mutate, modelMap }) {
|
||||||
}
|
}
|
||||||
dispatch(setCustomGpt({ chatGptLabel, promptPrefix }));
|
dispatch(setCustomGpt({ chatGptLabel, promptPrefix }));
|
||||||
dispatch(setModel('chatgptCustom'));
|
dispatch(setModel('chatgptCustom'));
|
||||||
|
handleSaveState(chatGptLabel.toLowerCase());
|
||||||
// dispatch(setDisabled(false));
|
// dispatch(setDisabled(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveHandler = (e) => {
|
const saveHandler = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
setModelSave(true);
|
||||||
const value = chatGptLabel.toLowerCase();
|
const value = chatGptLabel.toLowerCase();
|
||||||
|
|
||||||
if (chatGptLabel.length === 0) {
|
if (chatGptLabel.length === 0) {
|
||||||
|
|
@ -60,8 +63,15 @@ export default function ModelDialog({ mutate, modelMap }) {
|
||||||
// dispatch(setDisabled(false));
|
// dispatch(setDisabled(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (modelMap[chatGptLabel.toLowerCase()] && saveText === 'Save') {
|
if (
|
||||||
|
chatGptLabel !== 'chatgptCustom' &&
|
||||||
|
modelMap[chatGptLabel.toLowerCase()] &&
|
||||||
|
!initial[chatGptLabel.toLowerCase()] &&
|
||||||
|
saveText === 'Save'
|
||||||
|
) {
|
||||||
setSaveText('Update');
|
setSaveText('Update');
|
||||||
|
} else if (!modelMap[chatGptLabel.toLowerCase()] && saveText === 'Update') {
|
||||||
|
setSaveText('Save');
|
||||||
}
|
}
|
||||||
|
|
||||||
const requiredProp = required ? { required: true } : {};
|
const requiredProp = required ? { required: true } : {};
|
||||||
152
client/src/components/Models/ModelItem.jsx
Normal file
152
client/src/components/Models/ModelItem.jsx
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
import React, { useState, useRef } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { DropdownMenuRadioItem } from '../ui/DropdownMenu.tsx';
|
||||||
|
import { Circle } from 'lucide-react';
|
||||||
|
import { DialogTrigger } from '../ui/Dialog.tsx';
|
||||||
|
import RenameButton from '../Conversations/RenameButton';
|
||||||
|
import TrashIcon from '../svg/TrashIcon';
|
||||||
|
import manualSWR from '~/utils/fetchers';
|
||||||
|
|
||||||
|
export default function ModelItem({ modelName, value, onSelect }) {
|
||||||
|
const { customModel } = useSelector((state) => state.submit);
|
||||||
|
const { initial } = useSelector((state) => state.models);
|
||||||
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
|
const [renaming, setRenaming] = useState(false);
|
||||||
|
const [currentName, setCurrentName] = useState(modelName);
|
||||||
|
const [modelInput, setModelInput] = useState(modelName);
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
const rename = manualSWR(`http://localhost:3080/api/customGpts`, 'post');
|
||||||
|
const deleteCustom = manualSWR(`http://localhost:3080/api/customGpts/delete`, 'post');
|
||||||
|
|
||||||
|
if (value === 'chatgptCustom') {
|
||||||
|
return (
|
||||||
|
<DialogTrigger className="w-full">
|
||||||
|
<DropdownMenuRadioItem
|
||||||
|
value={value}
|
||||||
|
className="dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
|
||||||
|
>
|
||||||
|
{modelName}
|
||||||
|
<sup>$</sup>
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initial[value]) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuRadioItem
|
||||||
|
value={value}
|
||||||
|
className="dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
|
||||||
|
>
|
||||||
|
{modelName}
|
||||||
|
{value === 'chatgpt' && <sup>$</sup>}
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseOver = () => {
|
||||||
|
setIsHovering(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseOut = () => {
|
||||||
|
setIsHovering(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renameHandler = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setRenaming(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}, 25);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRename = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setRenaming(false);
|
||||||
|
if (modelInput === modelName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rename.trigger({
|
||||||
|
prevLabel: currentName,
|
||||||
|
chatGptLabel: modelInput,
|
||||||
|
value: modelInput.toLowerCase()
|
||||||
|
});
|
||||||
|
setCurrentName(modelInput);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await deleteCustom.trigger({ value: currentName.toLowerCase() });
|
||||||
|
// await mutate();
|
||||||
|
onSelect('chatgpt', true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
onRename(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonClass = {
|
||||||
|
className:
|
||||||
|
'z-50 rounded-md m-0 text-gray-400 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemClass = {
|
||||||
|
className:
|
||||||
|
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none hover:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:hover:bg-slate-700 dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800'
|
||||||
|
};
|
||||||
|
|
||||||
|
const showButtons = isHovering && !initial[value];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
value={value}
|
||||||
|
className={itemClass.className}
|
||||||
|
onClick={(e) => {
|
||||||
|
onSelect(value, true);
|
||||||
|
}}
|
||||||
|
onMouseOver={handleMouseOver}
|
||||||
|
onMouseOut={handleMouseOut}
|
||||||
|
>
|
||||||
|
{customModel === value && (
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{renaming === true ? (
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
className="pointer-events-auto z-50 m-0 mr-2 w-3/4 border border-blue-500 bg-transparent p-0 text-sm leading-tight outline-none"
|
||||||
|
value={modelInput}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onChange={(e) => setModelInput(e.target.value)}
|
||||||
|
onBlur={onRename}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
modelInput
|
||||||
|
)}
|
||||||
|
|
||||||
|
{value === 'chatgpt' && <sup>$</sup>}
|
||||||
|
{showButtons && (
|
||||||
|
<>
|
||||||
|
<RenameButton
|
||||||
|
twcss={`ml-auto mr-2 ${buttonClass.className}`}
|
||||||
|
onRename={onRename}
|
||||||
|
renaming={renaming}
|
||||||
|
renameHandler={renameHandler}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
{...buttonClass}
|
||||||
|
onClick={onDelete}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { setModel, setDisabled, setCustomGpt, setCustomModel } from '~/store/submitSlice';
|
import { setModel, setDisabled, setCustomGpt, setCustomModel } from '~/store/submitSlice';
|
||||||
import { setConversation } from '~/store/convoSlice';
|
import { setConversation } from '~/store/convoSlice';
|
||||||
|
|
@ -23,14 +23,11 @@ import { Dialog } from '../ui/Dialog.tsx';
|
||||||
|
|
||||||
export default function ModelMenu() {
|
export default function ModelMenu() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const [modelSave, setModelSave] = useState(false);
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const { model, customModel } = useSelector((state) => state.submit);
|
const { model, customModel } = useSelector((state) => state.submit);
|
||||||
const { models, modelMap, initial } = useSelector((state) => state.models);
|
const { models, modelMap, initial } = useSelector((state) => state.models);
|
||||||
const { trigger } = manualSWR('http://localhost:3050/customGpts', 'get', (res) => {
|
const { trigger } = manualSWR(`http://localhost:3080/api/customGpts`, 'get', (res) => {
|
||||||
console.log('models data (response)', res);
|
|
||||||
if (models.length + res.length === models.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchedModels = res.map((modelItem) => ({
|
const fetchedModels = res.map((modelItem) => ({
|
||||||
...modelItem,
|
...modelItem,
|
||||||
name: modelItem.chatGptLabel
|
name: modelItem.chatGptLabel
|
||||||
|
|
@ -53,7 +50,7 @@ export default function ModelMenu() {
|
||||||
localStorage.setItem('model', JSON.stringify(model));
|
localStorage.setItem('model', JSON.stringify(model));
|
||||||
}, [model]);
|
}, [model]);
|
||||||
|
|
||||||
const onChange = (value) => {
|
const onChange = (value, custom = false) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return;
|
return;
|
||||||
} else if (value === 'chatgptCustom') {
|
} else if (value === 'chatgptCustom') {
|
||||||
|
|
@ -61,15 +58,21 @@ export default function ModelMenu() {
|
||||||
} else if (initial[value]) {
|
} else if (initial[value]) {
|
||||||
dispatch(setModel(value));
|
dispatch(setModel(value));
|
||||||
dispatch(setDisabled(false));
|
dispatch(setDisabled(false));
|
||||||
setCustomModel(null);
|
dispatch(setCustomModel(null));
|
||||||
|
if (custom) {
|
||||||
|
trigger();
|
||||||
|
}
|
||||||
} else if (!initial[value]) {
|
} else if (!initial[value]) {
|
||||||
const chatGptLabel = modelMap[value]?.chatGptLabel;
|
const chatGptLabel = modelMap[value]?.chatGptLabel;
|
||||||
const promptPrefix = modelMap[value]?.promptPrefix;
|
const promptPrefix = modelMap[value]?.promptPrefix;
|
||||||
dispatch(setCustomGpt({ chatGptLabel, promptPrefix }));
|
dispatch(setCustomGpt({ chatGptLabel, promptPrefix }));
|
||||||
dispatch(setModel('chatgptCustom'));
|
dispatch(setModel('chatgptCustom'));
|
||||||
setCustomModel(value);
|
dispatch(setCustomModel(value));
|
||||||
|
if (custom) {
|
||||||
|
setMenuOpen((prevOpen) => !prevOpen);
|
||||||
|
}
|
||||||
} else if (!modelMap[value]) {
|
} else if (!modelMap[value]) {
|
||||||
setCustomModel(null);
|
dispatch(setCustomModel(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set new conversation
|
// Set new conversation
|
||||||
|
|
@ -83,10 +86,27 @@ export default function ModelMenu() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onOpenChange = (open) => {
|
||||||
|
if (!open) {
|
||||||
|
setModelSave(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveState = (value) => {
|
||||||
|
if (!modelSave) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(setCustomModel(value));
|
||||||
|
setModelSave(false);
|
||||||
|
};
|
||||||
|
|
||||||
const defaultColorProps = [
|
const defaultColorProps = [
|
||||||
'text-gray-500',
|
'text-gray-500',
|
||||||
'hover:bg-gray-100',
|
'hover:bg-gray-100',
|
||||||
|
'hover:bg-opacity-20',
|
||||||
'disabled:hover:bg-transparent',
|
'disabled:hover:bg-transparent',
|
||||||
|
'dark:data-[state=open]:bg-gray-800',
|
||||||
'dark:hover:bg-opacity-20',
|
'dark:hover:bg-opacity-20',
|
||||||
'dark:hover:bg-gray-900',
|
'dark:hover:bg-gray-900',
|
||||||
'dark:hover:text-gray-400',
|
'dark:hover:text-gray-400',
|
||||||
|
|
@ -95,9 +115,11 @@ export default function ModelMenu() {
|
||||||
|
|
||||||
const chatgptColorProps = [
|
const chatgptColorProps = [
|
||||||
'text-green-700',
|
'text-green-700',
|
||||||
|
'data-[state=open]:bg-green-100',
|
||||||
'dark:text-emerald-300',
|
'dark:text-emerald-300',
|
||||||
'hover:bg-green-100',
|
'hover:bg-green-100',
|
||||||
'disabled:hover:bg-transparent',
|
'disabled:hover:bg-transparent',
|
||||||
|
'dark:data-[state=open]:bg-green-900',
|
||||||
'dark:hover:bg-opacity-50',
|
'dark:hover:bg-opacity-50',
|
||||||
'dark:hover:bg-green-900',
|
'dark:hover:bg-green-900',
|
||||||
'dark:hover:text-gray-100',
|
'dark:hover:text-gray-100',
|
||||||
|
|
@ -108,15 +130,18 @@ export default function ModelMenu() {
|
||||||
const icon = model === 'bingai' ? <BingIcon button={true} /> : <GPTIcon button={true} />;
|
const icon = model === 'bingai' ? <BingIcon button={true} /> : <GPTIcon button={true} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog onOpenChange={onOpenChange}>
|
||||||
<DropdownMenu >
|
<DropdownMenu
|
||||||
|
open={menuOpen}
|
||||||
|
onOpenChange={setMenuOpen}
|
||||||
|
>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
// style={{backgroundColor: 'rgb(16, 163, 127)'}}
|
// style={{backgroundColor: 'rgb(16, 163, 127)'}}
|
||||||
className={`absolute bottom-0.5 rounded-md border-0 p-1 pl-2 outline-none ${colorProps.join(
|
className={`absolute bottom-0.5 rounded-md border-0 p-1 pl-2 outline-none ${colorProps.join(
|
||||||
' '
|
' '
|
||||||
)} focus:ring-0 focus:ring-offset-0 disabled:bottom-0.5 dark:data-[state=open]:bg-gray-800 dark:data-[state=open]:bg-opacity-50 md:bottom-1 md:left-2 md:pl-1 md:disabled:bottom-1`}
|
)} focus:ring-0 focus:ring-offset-0 disabled:bottom-0.5 dark:data-[state=open]:bg-opacity-50 md:bottom-1 md:left-2 md:pl-1 md:disabled:bottom-1`}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -129,11 +154,19 @@ export default function ModelMenu() {
|
||||||
onValueChange={onChange}
|
onValueChange={onChange}
|
||||||
className="overflow-y-auto"
|
className="overflow-y-auto"
|
||||||
>
|
>
|
||||||
<MenuItems models={models} />
|
<MenuItems
|
||||||
|
models={models}
|
||||||
|
onSelect={onChange}
|
||||||
|
/>
|
||||||
</DropdownMenuRadioGroup>
|
</DropdownMenuRadioGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<ModelDialog mutate={trigger} modelMap={modelMap}/>
|
<ModelDialog
|
||||||
|
mutate={trigger}
|
||||||
|
modelMap={modelMap}
|
||||||
|
setModelSave={setModelSave}
|
||||||
|
handleSaveState={handleSaveState}
|
||||||
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,27 +1,31 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TrashIcon from '../svg/TrashIcon';
|
import TrashIcon from '../svg/TrashIcon';
|
||||||
import { useSWRConfig } from "swr"
|
import { useSWRConfig } from 'swr';
|
||||||
import manualSWR from '~/utils/fetchers';
|
import manualSWR from '~/utils/fetchers';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { setConversation } from '~/store/convoSlice';
|
import { setConversation, removeAll } from '~/store/convoSlice';
|
||||||
import { setMessages } from '~/store/messageSlice';
|
import { setMessages } from '~/store/messageSlice';
|
||||||
|
|
||||||
export default function ClearConvos() {
|
export default function ClearConvos() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { mutate } = useSWRConfig()
|
const { mutate } = useSWRConfig();
|
||||||
|
|
||||||
const { trigger } = manualSWR(
|
const { trigger } = manualSWR(`http://localhost:3080/api/convos/clear`, 'post', () => {
|
||||||
'http://localhost:3050/convos/clear',
|
dispatch(setMessages([]));
|
||||||
'post',
|
dispatch(
|
||||||
() => {
|
setConversation({
|
||||||
dispatch(setMessages([]));
|
error: false,
|
||||||
dispatch(setConversation({ error: false, title: 'New chat', conversationId: null, parentMessageId: null }));
|
title: 'New chat',
|
||||||
mutate('http://localhost:3050/convos');
|
conversationId: null,
|
||||||
}
|
parentMessageId: null
|
||||||
);
|
})
|
||||||
|
);
|
||||||
|
mutate(`http://localhost:3080/api/convos`);
|
||||||
|
});
|
||||||
|
|
||||||
const clickHandler = () => {
|
const clickHandler = () => {
|
||||||
console.log('Clearing conversations...');
|
console.log('Clearing conversations...');
|
||||||
|
dispatch(removeAll());
|
||||||
trigger({});
|
trigger({});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -17,17 +17,20 @@ export default function Nav() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, isLoading, mutate } = swr(
|
const { data, isLoading, mutate } = swr(
|
||||||
`http://localhost:3050/convos?pageNumber=${pageNumber}`
|
`http://localhost:3080/api/convos?pageNumber=${pageNumber}`,
|
||||||
, onSuccess);
|
onSuccess
|
||||||
|
);
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const scrollPositionRef = useRef(null);
|
const scrollPositionRef = useRef(null);
|
||||||
|
|
||||||
const showMore = async () => {
|
const showMore = async (increment = true) => {
|
||||||
const container = containerRef.current;
|
if (increment) {
|
||||||
if (container) {
|
const container = containerRef.current;
|
||||||
scrollPositionRef.current = container.scrollTop;
|
if (container) {
|
||||||
|
scrollPositionRef.current = container.scrollTop;
|
||||||
|
}
|
||||||
|
dispatch(incrementPage());
|
||||||
}
|
}
|
||||||
dispatch(incrementPage());
|
|
||||||
await mutate();
|
await mutate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -44,9 +47,10 @@ export default function Nav() {
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const containerClasses = isLoading && pageNumber === 1
|
const containerClasses =
|
||||||
? 'flex flex-col gap-2 text-gray-100 text-sm h-full justify-center items-center'
|
isLoading && pageNumber === 1
|
||||||
: 'flex flex-col gap-2 text-gray-100 text-sm';
|
? 'flex flex-col gap-2 text-gray-100 text-sm h-full justify-center items-center'
|
||||||
|
: 'flex flex-col gap-2 text-gray-100 text-sm';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark hidden bg-gray-900 md:fixed md:inset-y-0 md:flex md:w-[260px] md:flex-col">
|
<div className="dark hidden bg-gray-900 md:fixed md:inset-y-0 md:flex md:w-[260px] md:flex-col">
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue