Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
191409209b | ||
|
|
04c52cba88 | ||
|
|
1d2ebb30bb | ||
|
|
53d639a9f6 | ||
|
|
47951851c5 | ||
|
|
f1b5f8cf3c | ||
|
|
e6a8868f6c | ||
|
|
e023a13bbc | ||
|
|
69dacca6c5 | ||
|
|
76b865646c | ||
|
|
9fe7943152 | ||
|
|
534ecb132c | ||
|
|
04fa7394f6 | ||
|
|
b9ed2fd785 | ||
|
|
e24a99481e | ||
|
|
405a4582b5 | ||
|
|
09f470111e | ||
|
|
ba604e8389 | ||
|
|
d9caaefdef | ||
|
|
a3359be316 | ||
|
|
880b111001 | ||
|
|
918b87979c | ||
|
|
385bcaf603 | ||
|
|
81e120ac47 | ||
|
|
1320d69cfb | ||
|
|
208376a418 | ||
|
|
6e3a89468c | ||
|
|
f953704831 | ||
|
|
1d7098a0cb | ||
|
|
e9f554dc4e | ||
|
|
55279def0d | ||
|
|
fa14276d0a | ||
|
|
8718dc4ed1 | ||
|
|
fe814acfd9 | ||
|
|
1e4f14c9b7 | ||
|
|
137ca5ae1a | ||
|
|
8a9b705b99 | ||
|
|
82c1811034 | ||
|
|
0d6aef6872 | ||
|
|
3f3ab8c33b | ||
|
|
6522536291 | ||
|
|
2bca5a032c | ||
|
|
53460bd891 | ||
|
|
fb9e8b8c7d | ||
|
|
21dc2b9236 | ||
|
|
1a6bf1d239 | ||
|
|
3e3283029d | ||
|
|
16c9b0e230 | ||
|
|
836df995d0 | ||
|
|
5b9d52b177 | ||
|
|
deb627a9ab | ||
|
|
70efc09dae | ||
|
|
8ff914582a | ||
|
|
f20a3562f3 | ||
|
|
4a1adf6d00 | ||
|
|
ddce1c9721 | ||
|
|
f67ed7621c | ||
|
|
97649e4bee | ||
|
|
1082da050b | ||
|
|
d89d1e288d | ||
|
|
cd89d11d0b | ||
|
|
cf0053a060 | ||
|
|
019da4399e | ||
|
|
044961bb01 | ||
|
|
2374c81edb | ||
|
|
699760713e | ||
|
|
d75413cc49 | ||
|
|
8175f199d2 | ||
|
|
f8c2f396c1 | ||
|
|
8217647df8 | ||
|
|
288c9eeeca | ||
|
|
4d09ff7c8a | ||
|
|
5fa059017c |
@@ -1,4 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
database.sqlite
|
dist
|
||||||
.idea
|
.idea
|
||||||
|
.output
|
||||||
|
.nuxt
|
||||||
.env
|
.env
|
||||||
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
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: https://www.buymeacoffee.com/WongSaang
|
||||||
36
.github/workflows/docker-image-static.yml
vendored
Normal file
36
.github/workflows/docker-image-static.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Docker Image CI - static
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
-
|
||||||
|
name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
-
|
||||||
|
name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
-
|
||||||
|
name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
-
|
||||||
|
name: Build and push
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: static.Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: wongsaang/chatgpt-ui-client:latest-static,wongsaang/chatgpt-ui-client:${{ github.ref_name }}-static
|
||||||
38
.github/workflows/docs.yml
vendored
Normal file
38
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, docs]
|
||||||
|
paths:
|
||||||
|
- 'docs/**'
|
||||||
|
- '.github/workflows/docs.yml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup yarn
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
cache: yarn
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: Build VuePress site
|
||||||
|
run: yarn docs:build
|
||||||
|
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
uses: crazy-max/ghaction-github-pages@v2
|
||||||
|
with:
|
||||||
|
target_branch: gh-pages
|
||||||
|
build_dir: docs/.vuepress/dist
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.ACTION_TOKEN }}
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,4 +7,5 @@ node_modules
|
|||||||
.env
|
.env
|
||||||
.idea
|
.idea
|
||||||
dist
|
dist
|
||||||
database.sqlite
|
.temp
|
||||||
|
.cache
|
||||||
|
|||||||
16
Dockerfile
16
Dockerfile
@@ -4,19 +4,23 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json yarn.lock ./
|
||||||
|
|
||||||
RUN yarn install
|
RUN yarn install && yarn cache clean
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN yarn generate
|
RUN yarn build
|
||||||
|
|
||||||
|
|
||||||
FROM nginx:alpine
|
FROM node:18-alpine3.16
|
||||||
|
|
||||||
|
ENV NITRO_PORT=80
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /app/.output/public .
|
COPY --from=builder /app/.output/ .
|
||||||
|
|
||||||
COPY nginx.conf /etc/nginx/templates/default.conf.template
|
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
|
# TODO: You can use NITRO_PRESET=node_cluster in order to leverage multi-process performance using Node.js cluster module. https://nuxt.com/docs/getting-started/deployment
|
||||||
|
|
||||||
|
ENTRYPOINT ["node", "server/index.mjs"]
|
||||||
202
README.md
202
README.md
@@ -2,202 +2,14 @@
|
|||||||
<h1>ChatGPT UI</h1>
|
<h1>ChatGPT UI</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
[English](./README.md) | [中文](./docs/zh/README.md)
|
A ChatGPT web client that supports multiple users, multiple languages, and multiple database connections for persistent data storage.
|
||||||
|
|
||||||
|
The server of this project:[https://github.com/WongSaang/chatgpt-ui-server](https://github.com/WongSaang/chatgpt-ui-server)
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
- [English](https://wongsaang.github.io/chatgpt-ui/)
|
||||||
|
- [中文](https://wongsaang.github.io/chatgpt-ui/zh/)
|
||||||
|
|
||||||
A ChatGPT web client that supports multiple users, multiple database connections for persistent data storage, supports i18n. Provides Docker images and quick deployment scripts.
|
|
||||||
|
|
||||||
https://user-images.githubusercontent.com/46235412/227156264-ca17ab17-999b-414f-ab06-3f75b5235bfe.mp4
|
https://user-images.githubusercontent.com/46235412/227156264-ca17ab17-999b-414f-ab06-3f75b5235bfe.mp4
|
||||||
|
|
||||||
|
|
||||||
## 📢Updates
|
|
||||||
<details open>
|
|
||||||
<summary><strong>2023-03-23</strong></summary>
|
|
||||||
Added web search capability to generate more relevant and up-to-date answers from ChatGPT!
|
|
||||||
This feature is off by default, you can turn it on in `Chat->Settings` in the admin panel, there is a record `open_web_search` in Settings, set its value to True.
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open>
|
|
||||||
<summary><strong>2023-03-15</strong></summary>
|
|
||||||
|
|
||||||
Add "open_registration" setting option in the admin panel to control whether user registration is enabled. You can log in to the admin panel and find this setting option under `Chat->Setting`. The default value of this setting is `True` (allow user registration). If you do not need it, please change it to `False`.
|
|
||||||
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open>
|
|
||||||
<summary><strong>2023-03-10</strong></summary>
|
|
||||||
|
|
||||||
Add 2 environment variables to control the typewriter effect:
|
|
||||||
|
|
||||||
- `NUXT_PUBLIC_TYPEWRITER=true` to enable/disable the typewriter effect
|
|
||||||
- `NUXT_PUBLIC_TYPEWRITER_DELAY=50` to set the delay time for each character in milliseconds.
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open>
|
|
||||||
<summary><strong>2023-03-04</strong></summary>
|
|
||||||
|
|
||||||
**Update to the latest official chat model** `gpt-3.5-turbo`
|
|
||||||
|
|
||||||
**🎉🎉🎉Provide a shell script that can be used to quickly deploy the service to server** [Quick start](#one-click-depolyment)
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
|
|
||||||
<summary><strong>2023-02-24</strong></summary>
|
|
||||||
Version 2 is a major update that separates the backend functionality as an independent project, hosted at [chatgpt-ui-server](https://github.com/WongSaang/chatgpt-ui-server).
|
|
||||||
|
|
||||||
If you still wish to use the old version, please visit the [v1 branch](https://github.com/WongSaang/chatgpt-ui/tree/v1).
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Version 2 introduces the following new features:
|
|
||||||
|
|
||||||
- 😉 Separation of the frontend and backend, with the backend now using the Python-based Django framework.
|
|
||||||
- 😘 User authentication, supporting multiple users.
|
|
||||||
- 😀 Ability to store data in an external database (defaulting to Sqlite).
|
|
||||||
- 😎 Session persistence, allowing the API to answer questions based on your context.
|
|
||||||
|
|
||||||
## 🚀 One-click deployment <a name="one-click-depolyment"></a>
|
|
||||||
|
|
||||||
Note: This script has only been tested on Ubuntu Server 22.04 LTS.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash <(curl -Ls https://raw.githubusercontent.com/WongSaang/chatgpt-ui/main/deployment.sh)
|
|
||||||
```
|
|
||||||
|
|
||||||
> If you have a domain name, you can point it to the server's IP address using DNS resolution. Of course, using the server's IP address directly is also possible.
|
|
||||||
> During the script's execution, you will be prompted to enter a domain name. If you do not have a domain name, you can enter the server's IP address directly.
|
|
||||||
|
|
||||||
### After the deployment is complete
|
|
||||||
|
|
||||||
Access `http(s)://your.domain:9000/admin` / IP `http(s)://123.123.123.123:9000/admin` to log in to the administration panel.
|
|
||||||
|
|
||||||
Default superuser: `admin`
|
|
||||||
|
|
||||||
Default password: `password`
|
|
||||||
|
|
||||||
Before you can start chatting, you need to add an OpenAI API key. In the Settings model, add a record with the name `openai_api_key` and the value as your API key.
|
|
||||||
|
|
||||||
Now you can access the web client at `http(s)://your.domain` or `http://123.123.123.123` to start chatting.
|
|
||||||
|
|
||||||
🎉🎉🎉 Enjoy it!
|
|
||||||
|
|
||||||
## Quick start with Docker Compose
|
|
||||||
|
|
||||||
### Run services
|
|
||||||
|
|
||||||
Below is a docker-compose.yml template:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3'
|
|
||||||
services:
|
|
||||||
client:
|
|
||||||
image: wongsaang/chatgpt-ui-client:latest
|
|
||||||
environment:
|
|
||||||
- SERVER_DOMAIN=http://backend-web-server
|
|
||||||
- NUXT_PUBLIC_APP_NAME='ChatGPT UI' # App name
|
|
||||||
- NUXT_PUBLIC_TYPEWRITER=true # Enable typewriter effect, default is false
|
|
||||||
- NUXT_PUBLIC_TYPEWRITER_DELAY=100 # Typewriter effect delay time, default is 50ms
|
|
||||||
depends_on:
|
|
||||||
- backend-web-server
|
|
||||||
ports:
|
|
||||||
- '80:80'
|
|
||||||
networks:
|
|
||||||
- chatgpt_ui_network
|
|
||||||
backend-wsgi-server:
|
|
||||||
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
|
||||||
environment:
|
|
||||||
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000} # CSRF whitelist,Add the address of your chatgpt-ui-web-server here, default is localhost:9000
|
|
||||||
#- DB_URL=postgres://postgres:postgrespw@localhost:49153/chatgpt # If this parameter is not set, the built-in Sqlite will be used by default. It should be noted that if you do not connect to an external database, the data will be lost after the container is destroyed.
|
|
||||||
#- OPENAI_API_PROXY=https://openai.proxy.com/v1 # Proxy for https://api.openai.com/v1
|
|
||||||
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name
|
|
||||||
- DJANGO_SUPERUSER_PASSWORD=password # default superuser password
|
|
||||||
- DJANGO_SUPERUSER_EMAIL=admin@example.com # default superuser email
|
|
||||||
- ACCOUNT_EMAIL_VERIFICATION=none # Determines the e-mail verification method during signup – choose one of "none", "optional", or "mandatory". Default is "optional". If you don't need to verify the email, you can set it to "none".
|
|
||||||
# If you want to use the email verification function, you need to configure the following parameters
|
|
||||||
# - EMAIL_HOST=SMTP server address
|
|
||||||
# - EMAIL_PORT=SMTP server port
|
|
||||||
# - EMAIL_HOST_USER=
|
|
||||||
# - EMAIL_HOST_PASSWORD=
|
|
||||||
# - EMAIL_USE_TLS=True
|
|
||||||
# - EMAIL_FROM=no-reply@example.com #Default sender email address
|
|
||||||
ports:
|
|
||||||
- '8000:8000'
|
|
||||||
networks:
|
|
||||||
- chatgpt_ui_network
|
|
||||||
backend-web-server:
|
|
||||||
image: wongsaang/chatgpt-ui-web-server:latest
|
|
||||||
environment:
|
|
||||||
- BACKEND_URL=http://backend-wsgi-server:8000
|
|
||||||
ports:
|
|
||||||
- '9000:80'
|
|
||||||
depends_on:
|
|
||||||
- backend-wsgi-server
|
|
||||||
networks:
|
|
||||||
- chatgpt_ui_network
|
|
||||||
|
|
||||||
networks:
|
|
||||||
chatgpt_ui_network:
|
|
||||||
driver: bridge
|
|
||||||
```
|
|
||||||
|
|
||||||
### DB_URL schema
|
|
||||||
|
|
||||||
| Engine | URL |
|
|
||||||
|----------------------|--------------------------------------------------|
|
|
||||||
| PostgreSQL | ``postgres://USER:PASSWORD@HOST:PORT/NAME`` |
|
|
||||||
| MySQL | ``mysql://USER:PASSWORD@HOST:PORT/NAME`` |
|
|
||||||
| SQLite | ``sqlite:///PATH`` |
|
|
||||||
|
|
||||||
|
|
||||||
### Set API key
|
|
||||||
|
|
||||||
Access `http(s)://your.domain:9000/admin` / IP `http(s)://123.123.123.123:9000/admin` to log in to the administration panel.
|
|
||||||
|
|
||||||
Default superuser: `admin`
|
|
||||||
|
|
||||||
Default password: `password`
|
|
||||||
|
|
||||||
Before you can start chatting, you need to add an OpenAI API key. In the Settings model, add a record with the name `openai_api_key` and the value as your API key.
|
|
||||||
|
|
||||||
Now you can access the web client at `http(s)://your.domain` or `http://123.123.123.123` to start chatting.
|
|
||||||
|
|
||||||
## Donation
|
|
||||||
|
|
||||||
> If it is helpful to you, it is also helping me.
|
|
||||||
|
|
||||||
If you want to support me, Buy me a coffee ❤️ [https://www.buymeacoffee.com/WongSaang](https://www.buymeacoffee.com/WongSaang)
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img height="150" src="https://github.com/WongSaang/chatgpt-ui/blob/main/demos/bmc_qr.png?raw=true"/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Setup
|
|
||||||
|
|
||||||
Make sure to install the dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# yarn
|
|
||||||
yarn install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development Server
|
|
||||||
|
|
||||||
Start the development server on http://localhost:3000
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production
|
|
||||||
|
|
||||||
Build the application for production:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn build
|
|
||||||
```
|
|
||||||
|
|||||||
4
app.vue
4
app.vue
@@ -1,8 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
|
||||||
<NuxtLoadingIndicator />
|
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
|
<NuxtLoadingIndicator />
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
326
components/Conversation.vue
Normal file
326
components/Conversation.vue
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
<script setup>
|
||||||
|
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
|
||||||
|
|
||||||
|
const { $i18n, $settings } = useNuxtApp()
|
||||||
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
const currentModel = useCurrentModel()
|
||||||
|
const openaiApiKey = useApiKey()
|
||||||
|
const fetchingResponse = ref(false)
|
||||||
|
const messageQueue = []
|
||||||
|
const frugalMode = ref(true)
|
||||||
|
let isProcessingQueue = false
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
conversation: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const processMessageQueue = () => {
|
||||||
|
if (isProcessingQueue || messageQueue.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!props.conversation.messages[props.conversation.messages.length - 1].is_bot) {
|
||||||
|
props.conversation.messages.push({id: null, is_bot: true, message: ''})
|
||||||
|
}
|
||||||
|
isProcessingQueue = true
|
||||||
|
const nextMessage = messageQueue.shift()
|
||||||
|
if (runtimeConfig.public.typewriter) {
|
||||||
|
let wordIndex = 0;
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
props.conversation.messages[props.conversation.messages.length - 1].message += nextMessage[wordIndex]
|
||||||
|
wordIndex++
|
||||||
|
if (wordIndex === nextMessage.length) {
|
||||||
|
clearInterval(intervalId)
|
||||||
|
isProcessingQueue = false
|
||||||
|
processMessageQueue()
|
||||||
|
}
|
||||||
|
}, runtimeConfig.public.typewriterDelay)
|
||||||
|
} else {
|
||||||
|
props.conversation.messages[props.conversation.messages.length - 1].message += nextMessage
|
||||||
|
isProcessingQueue = false
|
||||||
|
processMessageQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ctrl
|
||||||
|
const abortFetch = () => {
|
||||||
|
if (ctrl) {
|
||||||
|
ctrl.abort()
|
||||||
|
}
|
||||||
|
fetchingResponse.value = false
|
||||||
|
}
|
||||||
|
const fetchReply = async (message) => {
|
||||||
|
ctrl = new AbortController()
|
||||||
|
|
||||||
|
let webSearchParams = {}
|
||||||
|
if (enableWebSearch.value) {
|
||||||
|
webSearchParams['web_search'] = {
|
||||||
|
ua: navigator.userAgent,
|
||||||
|
default_prompt: $i18n.t('webSearchDefaultPrompt')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = Object.assign({}, currentModel.value, {
|
||||||
|
openaiApiKey: $settings.open_api_key_setting === 'True' ? openaiApiKey.value : null,
|
||||||
|
message: message,
|
||||||
|
conversationId: props.conversation.id,
|
||||||
|
frugalMode: frugalMode.value
|
||||||
|
}, webSearchParams)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetchEventSource('/api/conversation/', {
|
||||||
|
signal: ctrl.signal,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
openWhenHidden: true,
|
||||||
|
onopen(response) {
|
||||||
|
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to send message. HTTP ${response.status} - ${response.statusText}`);
|
||||||
|
},
|
||||||
|
onclose() {
|
||||||
|
if (ctrl.signal.aborted === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to send message. Server closed the connection unexpectedly.`);
|
||||||
|
},
|
||||||
|
onerror(err) {
|
||||||
|
throw err;
|
||||||
|
},
|
||||||
|
async onmessage(message) {
|
||||||
|
const event = message.event
|
||||||
|
const data = JSON.parse(message.data)
|
||||||
|
|
||||||
|
if (event === 'error') {
|
||||||
|
abortFetch()
|
||||||
|
showSnackbar(data.error)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === 'userMessageId') {
|
||||||
|
props.conversation.messages[props.conversation.messages.length - 1].id = data.userMessageId
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === 'done') {
|
||||||
|
abortFetch()
|
||||||
|
props.conversation.messages[props.conversation.messages.length - 1].id = data.messageId
|
||||||
|
if (!props.conversation.id) {
|
||||||
|
props.conversation.id = data.conversationId
|
||||||
|
genTitle(props.conversation.id)
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
messageQueue.push(data.content)
|
||||||
|
processMessageQueue()
|
||||||
|
|
||||||
|
scrollChatWindow()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
abortFetch()
|
||||||
|
showSnackbar(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const grab = ref(null)
|
||||||
|
const scrollChatWindow = () => {
|
||||||
|
if (grab.value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
grab.value.scrollIntoView({behavior: 'smooth'})
|
||||||
|
}
|
||||||
|
|
||||||
|
const send = (message) => {
|
||||||
|
fetchingResponse.value = true
|
||||||
|
if (props.conversation.messages.length === 0) {
|
||||||
|
addConversation(props.conversation)
|
||||||
|
}
|
||||||
|
props.conversation.messages.push({message: message})
|
||||||
|
fetchReply(message)
|
||||||
|
scrollChatWindow()
|
||||||
|
}
|
||||||
|
const stop = () => {
|
||||||
|
abortFetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
const snackbar = ref(false)
|
||||||
|
const snackbarText = ref('')
|
||||||
|
const showSnackbar = (text) => {
|
||||||
|
snackbarText.value = text
|
||||||
|
snackbar.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = ref(null)
|
||||||
|
const usePrompt = (prompt) => {
|
||||||
|
editor.value.usePrompt(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteMessage = (index) => {
|
||||||
|
props.conversation.messages.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const enableWebSearch = ref(false)
|
||||||
|
|
||||||
|
|
||||||
|
onNuxtReady(() => {
|
||||||
|
currentModel.value = getCurrentModel()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="conversation">
|
||||||
|
<div
|
||||||
|
v-if="conversation.loadingMessages"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
color="primary"
|
||||||
|
></v-progress-circular>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div
|
||||||
|
v-if="conversation.messages"
|
||||||
|
ref="chatWindow"
|
||||||
|
>
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
v-for="(message, index) in conversation.messages" :key="index"
|
||||||
|
cols="12"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex align-center"
|
||||||
|
:class="message.is_bot ? 'justify-start' : 'justify-end'"
|
||||||
|
>
|
||||||
|
<MessageActions
|
||||||
|
v-if="!message.is_bot"
|
||||||
|
:message="message"
|
||||||
|
:message-index="index"
|
||||||
|
:use-prompt="usePrompt"
|
||||||
|
:delete-message="deleteMessage"
|
||||||
|
/>
|
||||||
|
<MsgContent :message="message" />
|
||||||
|
<MessageActions
|
||||||
|
v-if="message.is_bot"
|
||||||
|
:message="message"
|
||||||
|
:message-index="index"
|
||||||
|
:use-prompt="usePrompt"
|
||||||
|
:delete-message="deleteMessage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<div ref="grab" class="w-100" style="height: 200px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<v-footer
|
||||||
|
app
|
||||||
|
class="footer"
|
||||||
|
>
|
||||||
|
<div class="px-md-16 w-100 d-flex flex-column">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-btn
|
||||||
|
v-show="fetchingResponse"
|
||||||
|
icon="close"
|
||||||
|
title="stop"
|
||||||
|
class="mr-3"
|
||||||
|
@click="stop"
|
||||||
|
></v-btn>
|
||||||
|
<MsgEditor ref="editor" :send-message="send" :disabled="fetchingResponse" :loading="fetchingResponse" />
|
||||||
|
</div>
|
||||||
|
<v-toolbar
|
||||||
|
density="comfortable"
|
||||||
|
color="transparent"
|
||||||
|
>
|
||||||
|
<Prompt v-show="!fetchingResponse" :use-prompt="usePrompt" />
|
||||||
|
<v-switch
|
||||||
|
v-if="$settings.open_web_search === 'True'"
|
||||||
|
v-model="enableWebSearch"
|
||||||
|
inline
|
||||||
|
hide-details
|
||||||
|
color="primary"
|
||||||
|
:label="$t('webSearch')"
|
||||||
|
></v-switch>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<div
|
||||||
|
v-if="$settings.open_frugal_mode_control === 'True'"
|
||||||
|
class="d-flex align-center"
|
||||||
|
>
|
||||||
|
<v-switch
|
||||||
|
v-model="frugalMode"
|
||||||
|
inline
|
||||||
|
hide-details
|
||||||
|
color="primary"
|
||||||
|
:label="$t('frugalMode')"
|
||||||
|
></v-switch>
|
||||||
|
<v-dialog
|
||||||
|
transition="dialog-bottom-transition"
|
||||||
|
width="auto"
|
||||||
|
>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-icon
|
||||||
|
color="grey"
|
||||||
|
v-bind="props"
|
||||||
|
icon="help_outline"
|
||||||
|
class="ml-3"
|
||||||
|
></v-icon>
|
||||||
|
</template>
|
||||||
|
<template v-slot:default="{ isActive }">
|
||||||
|
<v-card>
|
||||||
|
<v-toolbar
|
||||||
|
color="primary"
|
||||||
|
:title="$t('frugalMode')"
|
||||||
|
></v-toolbar>
|
||||||
|
<v-card-text>
|
||||||
|
{{ $t('frugalModeTip') }}
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</v-toolbar>
|
||||||
|
</div>
|
||||||
|
</v-footer>
|
||||||
|
<v-snackbar
|
||||||
|
v-model="snackbar"
|
||||||
|
multi-line
|
||||||
|
location="top"
|
||||||
|
>
|
||||||
|
{{ snackbarText }}
|
||||||
|
|
||||||
|
<template v-slot:actions>
|
||||||
|
<v-btn
|
||||||
|
color="red"
|
||||||
|
variant="text"
|
||||||
|
@click="snackbar = false"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-snackbar>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.footer {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
const dialog = ref(false)
|
const dialog = ref(false)
|
||||||
const currentModel = useCurrentModel()
|
const currentModel = useCurrentModel()
|
||||||
const availableModels = [
|
const availableModels = [
|
||||||
DEFAULT_MODEL.name
|
'gpt-3.5-turbo',
|
||||||
|
'gpt-4'
|
||||||
]
|
]
|
||||||
|
const currentModelDefault = ref(MODELS[currentModel.value.name])
|
||||||
|
|
||||||
|
onNuxtReady(() => {
|
||||||
|
currentModel.value = getCurrentModel()
|
||||||
watch(currentModel, (newVal, oldVal) => {
|
watch(currentModel, (newVal, oldVal) => {
|
||||||
|
currentModelDefault.value = MODELS[newVal.name]
|
||||||
saveCurrentModel(newVal)
|
saveCurrentModel(newVal)
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -49,7 +56,7 @@ watch(currentModel, (newVal, oldVal) => {
|
|||||||
<div class="d-flex justify-space-between align-center">
|
<div class="d-flex justify-space-between align-center">
|
||||||
<v-list-subheader>{{ $t('temperature') }}</v-list-subheader>
|
<v-list-subheader>{{ $t('temperature') }}</v-list-subheader>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="currentModel.temperature"
|
v-model.number="currentModel.temperature"
|
||||||
hide-details
|
hide-details
|
||||||
single-line
|
single-line
|
||||||
density="compact"
|
density="compact"
|
||||||
@@ -78,22 +85,25 @@ watch(currentModel, (newVal, oldVal) => {
|
|||||||
<div class="d-flex justify-space-between align-center">
|
<div class="d-flex justify-space-between align-center">
|
||||||
<v-list-subheader>{{ $t('maxTokens') }}</v-list-subheader>
|
<v-list-subheader>{{ $t('maxTokens') }}</v-list-subheader>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="currentModel.max_tokens"
|
v-model.number="currentModel.max_tokens"
|
||||||
hide-details
|
hide-details
|
||||||
single-line
|
single-line
|
||||||
density="compact"
|
density="compact"
|
||||||
type="number"
|
type="number"
|
||||||
max="2048"
|
:max="currentModelDefault.total_tokens"
|
||||||
step="1"
|
step="1"
|
||||||
style="width: 100px"
|
style="width: 100px"
|
||||||
class="flex-grow-0"
|
class="flex-grow-0"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-caption">
|
||||||
|
{{ $t('maxTokenTips1') }} <b>{{ currentModelDefault.total_tokens }}</b> {{ $t('maxTokenTips2') }}
|
||||||
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-slider
|
<v-slider
|
||||||
v-model="currentModel.max_tokens"
|
v-model="currentModel.max_tokens"
|
||||||
:max="2048"
|
:max="currentModelDefault.total_tokens"
|
||||||
:step="1"
|
:step="1"
|
||||||
hide-details
|
hide-details
|
||||||
>
|
>
|
||||||
@@ -107,7 +117,7 @@ watch(currentModel, (newVal, oldVal) => {
|
|||||||
<div class="d-flex justify-space-between align-center">
|
<div class="d-flex justify-space-between align-center">
|
||||||
<v-list-subheader>{{ $t('topP') }}</v-list-subheader>
|
<v-list-subheader>{{ $t('topP') }}</v-list-subheader>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="currentModel.top_p"
|
v-model.number="currentModel.top_p"
|
||||||
hide-details
|
hide-details
|
||||||
single-line
|
single-line
|
||||||
density="compact"
|
density="compact"
|
||||||
@@ -134,7 +144,7 @@ watch(currentModel, (newVal, oldVal) => {
|
|||||||
<div class="d-flex justify-space-between align-center">
|
<div class="d-flex justify-space-between align-center">
|
||||||
<v-list-subheader>{{ $t('frequencyPenalty') }}</v-list-subheader>
|
<v-list-subheader>{{ $t('frequencyPenalty') }}</v-list-subheader>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="currentModel.frequency_penalty"
|
v-model.number="currentModel.frequency_penalty"
|
||||||
hide-details
|
hide-details
|
||||||
single-line
|
single-line
|
||||||
density="compact"
|
density="compact"
|
||||||
@@ -160,7 +170,7 @@ watch(currentModel, (newVal, oldVal) => {
|
|||||||
<div class="d-flex justify-space-between align-center">
|
<div class="d-flex justify-space-between align-center">
|
||||||
<v-list-subheader>{{ $t('presencePenalty') }}</v-list-subheader>
|
<v-list-subheader>{{ $t('presencePenalty') }}</v-list-subheader>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="currentModel.presence_penalty"
|
v-model.number="currentModel.presence_penalty"
|
||||||
hide-details
|
hide-details
|
||||||
single-line
|
single-line
|
||||||
density="compact"
|
density="compact"
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import hljs from "highlight.js"
|
import hljs from "highlight.js"
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
|
import mathjax3 from 'markdown-it-mathjax3'
|
||||||
|
|
||||||
|
|
||||||
const md = new MarkdownIt({
|
const md = new MarkdownIt({
|
||||||
@@ -11,6 +12,7 @@ const md = new MarkdownIt({
|
|||||||
return `<pre class="hljs-code-container my-3"><div class="hljs-code-header d-flex align-center justify-space-between bg-grey-darken-3 pa-1"><span class="pl-2 text-caption">${language}</span><button class="hljs-copy-button" data-copied="false">Copy</button></div><code class="hljs language-${language}">${hljs.highlight(code, { language: language, ignoreIllegals: true }).value}</code></pre>`
|
return `<pre class="hljs-code-container my-3"><div class="hljs-code-header d-flex align-center justify-space-between bg-grey-darken-3 pa-1"><span class="pl-2 text-caption">${language}</span><button class="hljs-copy-button" data-copied="false">Copy</button></div><code class="hljs language-${language}">${hljs.highlight(code, { language: language, ignoreIllegals: true }).value}</code></pre>`
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
md.use(mathjax3)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
message: {
|
message: {
|
||||||
@@ -23,8 +25,10 @@ const contentHtml = ref('')
|
|||||||
|
|
||||||
const contentElm = ref(null)
|
const contentElm = ref(null)
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(async () => {
|
||||||
contentHtml.value = props.message.message ? md.render(props.message.message) : ''
|
contentHtml.value = props.message.message ? md.render(props.message.message) : ''
|
||||||
|
await nextTick()
|
||||||
|
bindCopyCodeToButtons()
|
||||||
})
|
})
|
||||||
|
|
||||||
const bindCopyCodeToButtons = () => {
|
const bindCopyCodeToButtons = () => {
|
||||||
@@ -52,10 +56,6 @@ onMounted(() => {
|
|||||||
bindCopyCodeToButtons()
|
bindCopyCodeToButtons()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUpdated(() => {
|
|
||||||
bindCopyCodeToButtons()
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -64,18 +64,43 @@ onUpdated(() => {
|
|||||||
rounded="lg"
|
rounded="lg"
|
||||||
elevation="2"
|
elevation="2"
|
||||||
>
|
>
|
||||||
<v-card-text>
|
|
||||||
<div
|
<div
|
||||||
ref="contentElm"
|
ref="contentElm"
|
||||||
v-html="contentHtml"
|
v-html="contentHtml"
|
||||||
class="chat-msg-content"
|
class="chat-msg-content pa-3"
|
||||||
></div>
|
></div>
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.chat-msg-content ol {
|
.chat-msg-content {
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
}
|
||||||
|
.chat-msg-content p,
|
||||||
|
.chat-msg-content table,
|
||||||
|
.chat-msg-content ul,
|
||||||
|
.chat-msg-content ol,
|
||||||
|
.chat-msg-content h1,
|
||||||
|
.chat-msg-content h2,
|
||||||
|
.chat-msg-content h3,
|
||||||
|
.chat-msg-content h4,
|
||||||
|
.chat-msg-content h5,
|
||||||
|
.chat-msg-content h6 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.chat-msg-content table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-radius: .5rem;
|
||||||
|
}
|
||||||
|
.chat-msg-content table th,
|
||||||
|
.chat-msg-content table td {
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
border: 1px solid gray;
|
||||||
|
}
|
||||||
|
.chat-msg-content ol, .chat-msg-content ul {
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
}
|
}
|
||||||
.hljs-code-container {
|
.hljs-code-container {
|
||||||
@@ -93,4 +118,10 @@ onUpdated(() => {
|
|||||||
.hljs-copy-button:active{border-color:#ffffff66}
|
.hljs-copy-button:active{border-color:#ffffff66}
|
||||||
.hljs-copy-button[data-copied="true"]{text-indent:0;width:auto;background-image:none}
|
.hljs-copy-button[data-copied="true"]{text-indent:0;width:auto;background-image:none}
|
||||||
@media(prefers-reduced-motion){.hljs-copy-button{transition:none}}
|
@media(prefers-reduced-motion){.hljs-copy-button{transition:none}}
|
||||||
|
|
||||||
|
/*MathJax*/
|
||||||
|
.MathJax svg {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,8 +1,82 @@
|
|||||||
|
<script setup>
|
||||||
|
import { isMobile } from 'is-mobile'
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
sendMessage: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const message = ref('')
|
||||||
|
const rows = ref(1)
|
||||||
|
const autoGrow = ref(true)
|
||||||
|
|
||||||
|
const hint = computed(() => {
|
||||||
|
return isMobile() ? '' : $i18n.t('pressEnterToSendYourMessageOrShiftEnterToAddANewLine')
|
||||||
|
})
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
const lines = message.value.split(/\r\n|\r|\n/).length
|
||||||
|
if (lines > 8) {
|
||||||
|
rows.value = 8
|
||||||
|
autoGrow.value = false
|
||||||
|
} else {
|
||||||
|
rows.value = 1
|
||||||
|
autoGrow.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const send = () => {
|
||||||
|
let msg = message.value
|
||||||
|
// remove the last "\n"
|
||||||
|
if (msg[msg.length - 1] === "\n") {
|
||||||
|
msg = msg.slice(0, -1)
|
||||||
|
}
|
||||||
|
if (msg.length > 0) {
|
||||||
|
props.sendMessage(msg)
|
||||||
|
}
|
||||||
|
message.value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const textArea = ref()
|
||||||
|
|
||||||
|
const usePrompt = (prompt) => {
|
||||||
|
message.value = prompt
|
||||||
|
textArea.value.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickSendBtn = () => {
|
||||||
|
send()
|
||||||
|
}
|
||||||
|
|
||||||
|
const enterOnly = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!isMobile()) {
|
||||||
|
send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
usePrompt
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex-grow-1 d-flex align-center justify-space-between"
|
class="flex-grow-1 d-flex align-center justify-space-between"
|
||||||
>
|
>
|
||||||
<v-textarea
|
<v-textarea
|
||||||
|
ref="textArea"
|
||||||
v-model="message"
|
v-model="message"
|
||||||
:label="$t('writeAMessage')"
|
:label="$t('writeAMessage')"
|
||||||
:placeholder="hint"
|
:placeholder="hint"
|
||||||
@@ -25,67 +99,3 @@
|
|||||||
></v-btn>
|
></v-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import { isMobile } from 'is-mobile'
|
|
||||||
export default {
|
|
||||||
name: "MsgEditor",
|
|
||||||
props: {
|
|
||||||
sendMessage: Function,
|
|
||||||
disabled: Boolean,
|
|
||||||
loading: Boolean,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
message: "",
|
|
||||||
rows: 1,
|
|
||||||
autoGrow: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
hint() {
|
|
||||||
return isMobile() ? "" : "Press Enter to send your message or Shift+Enter to add a new line";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
message(val) {
|
|
||||||
const lines = val.split(/\r\n|\r|\n/).length;
|
|
||||||
if (lines > 8) {
|
|
||||||
this.rows = 8;
|
|
||||||
this.autoGrow = false;
|
|
||||||
} else {
|
|
||||||
this.rows = 1;
|
|
||||||
this.autoGrow = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
send() {
|
|
||||||
let msg = this.message
|
|
||||||
// remove the last "\n"
|
|
||||||
if (msg[msg.length - 1] === "\n") {
|
|
||||||
msg = msg.slice(0, -1)
|
|
||||||
}
|
|
||||||
if (msg.length > 0) {
|
|
||||||
this.sendMessage(msg)
|
|
||||||
}
|
|
||||||
this.message = ""
|
|
||||||
},
|
|
||||||
usePrompt(prompt) {
|
|
||||||
this.message = prompt
|
|
||||||
},
|
|
||||||
clickSendBtn () {
|
|
||||||
this.send()
|
|
||||||
},
|
|
||||||
enterOnly (event) {
|
|
||||||
event.preventDefault();
|
|
||||||
if (!isMobile()) {
|
|
||||||
this.send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
||||||
341
components/NavigationDrawer.vue
Normal file
341
components/NavigationDrawer.vue
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
import {useDrawer} from "../composables/states";
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const { $i18n, $settings } = useNuxtApp()
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
const {mdAndUp} = useDisplay()
|
||||||
|
const drawerPermanent = computed(() => {
|
||||||
|
return mdAndUp.value
|
||||||
|
})
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
const themes = ref([
|
||||||
|
{ title: $i18n.t('lightMode'), value: 'light' },
|
||||||
|
{ title: $i18n.t('darkMode'), value: 'dark' },
|
||||||
|
{ title: $i18n.t('followSystem'), value: 'system'}
|
||||||
|
])
|
||||||
|
const setTheme = (theme) => {
|
||||||
|
colorMode.preference = theme
|
||||||
|
}
|
||||||
|
const feedback = () => {
|
||||||
|
window.open('https://github.com/WongSaang/chatgpt-ui/issues', '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { locale, locales, setLocale } = useI18n()
|
||||||
|
const setLang = (lang) => {
|
||||||
|
setLocale(lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversations = useConversations()
|
||||||
|
|
||||||
|
const editingConversation = ref(null)
|
||||||
|
const deletingConversationIndex = ref(null)
|
||||||
|
|
||||||
|
const editConversation = (index) => {
|
||||||
|
editingConversation.value = conversations.value[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateConversation = async (index) => {
|
||||||
|
editingConversation.value.updating = true
|
||||||
|
const { data, error } = await useAuthFetch(`/api/chat/conversations/${editingConversation.value.id}/`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({
|
||||||
|
topic: editingConversation.value.topic
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!error.value) {
|
||||||
|
conversations.value[index] = editingConversation.value
|
||||||
|
}
|
||||||
|
editingConversation.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteConversation = async (index) => {
|
||||||
|
deletingConversationIndex.value = index
|
||||||
|
const { data, error } = await useAuthFetch(`/api/chat/conversations/${conversations.value[index].id}/`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
deletingConversationIndex.value = null
|
||||||
|
if (!error.value) {
|
||||||
|
const deletingConversation = conversations.value[index]
|
||||||
|
conversations.value.splice(index, 1)
|
||||||
|
if (route.params.id && parseInt(route.params.id) === deletingConversation.id) {
|
||||||
|
await navigateTo('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearConversations = async () => {
|
||||||
|
deletingConversations.value = true
|
||||||
|
const { data, error } = await useAuthFetch(`/api/chat/conversations/delete_all`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
if (!error.value) {
|
||||||
|
loadConversations()
|
||||||
|
clearConfirmDialog.value = false
|
||||||
|
}
|
||||||
|
deletingConversations.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearConfirmDialog = ref(false)
|
||||||
|
const deletingConversations = ref(false)
|
||||||
|
const loadingConversations = ref(false)
|
||||||
|
|
||||||
|
const loadConversations = async () => {
|
||||||
|
loadingConversations.value = true
|
||||||
|
conversations.value = await getConversations()
|
||||||
|
loadingConversations.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const signOut = async () => {
|
||||||
|
const { data, error } = await useFetch('/api/account/logout/', {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
if (!error.value) {
|
||||||
|
await logout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNuxtReady(async () => {
|
||||||
|
loadConversations()
|
||||||
|
})
|
||||||
|
|
||||||
|
const drawer = useDrawer()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-navigation-drawer
|
||||||
|
v-model="drawer"
|
||||||
|
:permanent="drawerPermanent"
|
||||||
|
width="300"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-slot:prepend
|
||||||
|
v-if="user"
|
||||||
|
>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
:title="user.username"
|
||||||
|
:subtitle="user.email"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon
|
||||||
|
icon="face"
|
||||||
|
size="x-large"
|
||||||
|
></v-icon>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-menu>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
v-bind="props"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
icon="expand_more"
|
||||||
|
></v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
:title="$t('resetPassword')"
|
||||||
|
to="/account/resetPassword"
|
||||||
|
>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item
|
||||||
|
:title="$t('signOut')"
|
||||||
|
@click="signOut"
|
||||||
|
>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="px-2">
|
||||||
|
<v-list>
|
||||||
|
<v-list-item v-show="loadingConversations">
|
||||||
|
<v-list-item-title class="d-flex justify-center">
|
||||||
|
<v-progress-circular indeterminate></v-progress-circular>
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<v-list>
|
||||||
|
<template
|
||||||
|
v-for="(conversation, cIdx) in conversations"
|
||||||
|
:key="conversation.id"
|
||||||
|
>
|
||||||
|
<v-list-item
|
||||||
|
active-color="primary"
|
||||||
|
rounded="xl"
|
||||||
|
v-if="editingConversation && editingConversation.id === conversation.id"
|
||||||
|
>
|
||||||
|
<v-text-field
|
||||||
|
v-model="editingConversation.topic"
|
||||||
|
:loading="editingConversation.updating"
|
||||||
|
variant="underlined"
|
||||||
|
append-icon="done"
|
||||||
|
hide-details
|
||||||
|
density="compact"
|
||||||
|
autofocus
|
||||||
|
@keyup.enter="updateConversation(cIdx)"
|
||||||
|
@click:append="updateConversation(cIdx)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-list-item>
|
||||||
|
<v-hover
|
||||||
|
v-if="!editingConversation || editingConversation.id !== conversation.id"
|
||||||
|
v-slot="{ isHovering, props }"
|
||||||
|
>
|
||||||
|
<v-list-item
|
||||||
|
rounded="xl"
|
||||||
|
active-color="primary"
|
||||||
|
:to="conversation.id ? `/${conversation.id}` : '/'"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<v-list-item-title>{{ (conversation.topic && conversation.topic !== '') ? conversation.topic : $t('defaultConversationTitle') }}</v-list-item-title>
|
||||||
|
<template v-slot:append>
|
||||||
|
<div
|
||||||
|
v-show="isHovering && conversation.id"
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
icon="edit"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click.prevent="editConversation(cIdx)"
|
||||||
|
>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon="delete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
:loading="deletingConversationIndex === cIdx"
|
||||||
|
@click.prevent="deleteConversation(cIdx)"
|
||||||
|
>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-hover>
|
||||||
|
</template>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-slot:append>
|
||||||
|
<div class="px-1">
|
||||||
|
<v-divider></v-divider>
|
||||||
|
<v-list>
|
||||||
|
|
||||||
|
<v-dialog
|
||||||
|
v-model="clearConfirmDialog"
|
||||||
|
persistent
|
||||||
|
>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-list-item
|
||||||
|
v-bind="props"
|
||||||
|
rounded="xl"
|
||||||
|
prepend-icon="delete_forever"
|
||||||
|
:title="$t('clearConversations')"
|
||||||
|
></v-list-item>
|
||||||
|
</template>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">
|
||||||
|
Are you sure you want to delete all conversations?
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>This will be a permanent deletion and cannot be retrieved once deleted. Please proceed with caution.</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
color="green-darken-1"
|
||||||
|
variant="text"
|
||||||
|
@click="clearConfirmDialog = false"
|
||||||
|
class="text-none"
|
||||||
|
>
|
||||||
|
Cancel deletion
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="green-darken-1"
|
||||||
|
variant="text"
|
||||||
|
@click="clearConversations"
|
||||||
|
class="text-none"
|
||||||
|
:loading="deletingConversations"
|
||||||
|
>
|
||||||
|
Confirm deletion
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<ApiKeyDialog
|
||||||
|
v-if="$settings.open_api_key_setting === 'True'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModelParameters/>
|
||||||
|
|
||||||
|
<v-menu
|
||||||
|
>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-list-item
|
||||||
|
v-bind="props"
|
||||||
|
rounded="xl"
|
||||||
|
:title="$t('themeMode')"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-slot:prepend
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
v-show="$colorMode.value === 'light'"
|
||||||
|
icon="light_mode"
|
||||||
|
></v-icon>
|
||||||
|
<v-icon
|
||||||
|
v-show="$colorMode.value !== 'light'"
|
||||||
|
icon="dark_mode"
|
||||||
|
></v-icon>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
<v-list
|
||||||
|
bg-color="white"
|
||||||
|
>
|
||||||
|
<v-list-item
|
||||||
|
v-for="(theme, idx) in themes"
|
||||||
|
:key="idx"
|
||||||
|
@click="setTheme(theme.value)"
|
||||||
|
>
|
||||||
|
<v-list-item-title>{{ theme.title }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
|
||||||
|
<SettingsLanguages/>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
rounded="xl"
|
||||||
|
prepend-icon="help_outline"
|
||||||
|
:title="$t('feedback')"
|
||||||
|
@click="feedback"
|
||||||
|
></v-list-item>
|
||||||
|
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.v-navigation-drawer__content::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
.v-navigation-drawer__content:hover::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
.v-navigation-drawer__content:hover::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #999;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
const menu = ref(false)
|
const menu = ref(false)
|
||||||
const prompts = ref([])
|
const prompts = ref([])
|
||||||
const editingPrompt = ref(null)
|
const editingPrompt = ref(null)
|
||||||
|
const newTitlePrompt = ref(null)
|
||||||
const newPrompt = ref('')
|
const newPrompt = ref('')
|
||||||
const submittingNewPrompt = ref(false)
|
const submittingNewPrompt = ref(false)
|
||||||
const promptInputErrorMessage = ref('')
|
const promptInputErrorMessage = ref('')
|
||||||
@@ -24,11 +25,13 @@ const addPrompt = async () => {
|
|||||||
const { data, error } = await useAuthFetch('/api/chat/prompts/', {
|
const { data, error } = await useAuthFetch('/api/chat/prompts/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
title: newTitlePrompt.value,
|
||||||
prompt: newPrompt.value
|
prompt: newPrompt.value
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if (!error.value) {
|
if (!error.value) {
|
||||||
prompts.value.push(data.value)
|
prompts.value.push(data.value)
|
||||||
|
newTitlePrompt.value = null
|
||||||
newPrompt.value = ''
|
newPrompt.value = ''
|
||||||
}
|
}
|
||||||
submittingNewPrompt.value = false
|
submittingNewPrompt.value = false
|
||||||
@@ -43,6 +46,7 @@ const updatePrompt = async (index) => {
|
|||||||
const { data, error } = await useAuthFetch(`/api/chat/prompts/${editingPrompt.value.id}/`, {
|
const { data, error } = await useAuthFetch(`/api/chat/prompts/${editingPrompt.value.id}/`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
title: editingPrompt.value.title,
|
||||||
prompt: editingPrompt.value.prompt
|
prompt: editingPrompt.value.prompt
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -82,7 +86,7 @@ const selectPrompt = (prompt) => {
|
|||||||
menu.value = false
|
menu.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted( () => {
|
onNuxtReady( () => {
|
||||||
loadPrompts()
|
loadPrompts()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -110,7 +114,7 @@ onMounted( () => {
|
|||||||
max-width="500"
|
max-width="500"
|
||||||
>
|
>
|
||||||
<v-card-title>
|
<v-card-title>
|
||||||
<span class="headline">Frequently prompts</span>
|
<span class="headline">{{ $t('frequentlyPrompts') }}</span>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
@@ -127,18 +131,30 @@ onMounted( () => {
|
|||||||
>
|
>
|
||||||
<v-list-item
|
<v-list-item
|
||||||
active-color="primary"
|
active-color="primary"
|
||||||
rounded="xl"
|
|
||||||
v-if="editingPrompt && editingPrompt.id === prompt.id"
|
v-if="editingPrompt && editingPrompt.id === prompt.id"
|
||||||
>
|
>
|
||||||
|
<div class="d-flex flex-row" :style="{ marginTop: '5px' }">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<v-text-field
|
||||||
|
v-model="editingPrompt.title"
|
||||||
|
:loading="editingPrompt.updating"
|
||||||
|
:label="$t('titlePrompt')"
|
||||||
|
variant="underlined"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
<v-textarea
|
<v-textarea
|
||||||
rows="2"
|
rows="2"
|
||||||
v-model="editingPrompt.prompt"
|
v-model="editingPrompt.prompt"
|
||||||
:loading="editingPrompt.updating"
|
:loading="editingPrompt.updating"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
hide-details
|
|
||||||
density="compact"
|
density="compact"
|
||||||
|
hide-details
|
||||||
>
|
>
|
||||||
<template v-slot:append>
|
</v-textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<v-btn
|
<v-btn
|
||||||
icon="done"
|
icon="done"
|
||||||
@@ -154,8 +170,8 @@ onMounted( () => {
|
|||||||
>
|
>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</v-textarea>
|
</div>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-if="!editingPrompt || editingPrompt.id !== prompt.id"
|
v-if="!editingPrompt || editingPrompt.id !== prompt.id"
|
||||||
@@ -163,7 +179,7 @@ onMounted( () => {
|
|||||||
active-color="primary"
|
active-color="primary"
|
||||||
@click="selectPrompt(prompt)"
|
@click="selectPrompt(prompt)"
|
||||||
>
|
>
|
||||||
<v-list-item-title>{{ prompt.prompt }}</v-list-item-title>
|
<v-list-item-title>{{ prompt.title ? prompt.title : prompt.prompt }}</v-list-item-title>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<v-btn
|
<v-btn
|
||||||
icon="edit"
|
icon="edit"
|
||||||
@@ -184,6 +200,25 @@ onMounted( () => {
|
|||||||
</v-list-item>
|
</v-list-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
active-color="primary"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="pt-3"
|
||||||
|
>
|
||||||
|
<v-text-field
|
||||||
|
rows="1"
|
||||||
|
v-model="newTitlePrompt"
|
||||||
|
:label="$t('titlePrompt')"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
</div>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
<v-list-item
|
<v-list-item
|
||||||
active-color="primary"
|
active-color="primary"
|
||||||
>
|
>
|
||||||
@@ -193,7 +228,7 @@ onMounted( () => {
|
|||||||
<v-textarea
|
<v-textarea
|
||||||
rows="2"
|
rows="2"
|
||||||
v-model="newPrompt"
|
v-model="newPrompt"
|
||||||
label="Add a new prompt"
|
:label="$t('addNewPrompt')"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
density="compact"
|
density="compact"
|
||||||
:error-messages="promptInputErrorMessage"
|
:error-messages="promptInputErrorMessage"
|
||||||
@@ -211,7 +246,7 @@ onMounted( () => {
|
|||||||
@click="addPrompt()"
|
@click="addPrompt()"
|
||||||
>
|
>
|
||||||
<v-icon icon="add"></v-icon>
|
<v-icon icon="add"></v-icon>
|
||||||
Add prompt
|
{{ $t('addPrompt') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
|
|||||||
@@ -4,10 +4,9 @@
|
|||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h2 class="text-h2">{{ $t('welcomeTo') }} <span class="text-primary">{{ runtimeConfig.public.appName }}</span></h2>
|
<h2 class="text-h2">{{ $t('welcomeTo') }} <span class="text-primary">{{ runtimeConfig.public.appName }}</span></h2>
|
||||||
<p class="text-caption mt-5">
|
<p class="text-caption my-5">
|
||||||
{{ runtimeConfig.public.appName }} {{ $t('welcomeScreen.introduction1') }}
|
{{ runtimeConfig.public.appName }} {{ $t('welcomeScreen.introduction1') }}
|
||||||
<br>
|
<br>
|
||||||
{{ $t('welcomeScreen.introduction2') }}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|||||||
18
composables/fetch.js
Normal file
18
composables/fetch.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export const useMyFetch = (url, options = {}) => {
|
||||||
|
let defaultOptions = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (process.server) {
|
||||||
|
defaultOptions.baseURL = process.env.SERVER_DOMAIN
|
||||||
|
}
|
||||||
|
return useFetch(url, Object.assign(defaultOptions, options))
|
||||||
|
}
|
||||||
|
export const useAuthFetch = async (url, options = {}) => {
|
||||||
|
const res = await useMyFetch(url, options)
|
||||||
|
if (res.error.value && res.error.value.status === 401) {
|
||||||
|
await logout()
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
|
|
||||||
export const useModels = () => useState('models', () => getStoredModels())
|
// export const useModels = () => useState('models', () => getStoredModels())
|
||||||
|
|
||||||
export const useCurrentModel = () => useState('currentModel', () => getCurrentModel())
|
export const useCurrentModel = () => useState('currentModel', () => getCurrentModel())
|
||||||
|
|
||||||
export const useApiKey = () => useState('apiKey', () => getStoredApiKey())
|
export const useApiKey = () => useState('apiKey', () => getStoredApiKey())
|
||||||
|
|
||||||
export const useConversion = () => useState('conversion', () => getDefaultConversionData())
|
export const useConversations = () => useState('conversations', () => [])
|
||||||
|
|
||||||
export const useConversions = () => useState('conversions', () => [])
|
export const useUser = () => useState('user', () => null)
|
||||||
|
|
||||||
export const useSettings = () => useState('settings', () => {})
|
export const useDrawer = () => useState('drawer', () => false)
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export const useAuthFetch = async (url, options = {}) => {
|
|
||||||
const { $auth } = useNuxtApp()
|
|
||||||
|
|
||||||
const res = await useFetch(url, options)
|
|
||||||
if (res.error.value && res.error.value.status === 401) {
|
|
||||||
await $auth.logout()
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
@@ -24,6 +24,13 @@ if [ -z "$WSGI_PORT" ]; then
|
|||||||
WSGI_PORT="8000"
|
WSGI_PORT="8000"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
read -p "If you want to connect to a database, please enter the database URL [default: none]: " DATABASE_URL
|
||||||
|
|
||||||
|
if [ -z "$DATABASE_URL" ]; then
|
||||||
|
DATABASE_URL="sqlite:///db.sqlite3"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
if [[ $(which docker) ]]; then
|
if [[ $(which docker) ]]; then
|
||||||
echo "Docker is already installed"
|
echo "Docker is already installed"
|
||||||
else
|
else
|
||||||
@@ -63,8 +70,12 @@ echo "Downloading configuration files..."
|
|||||||
|
|
||||||
sudo curl -L "https://raw.githubusercontent.com/WongSaang/chatgpt-ui/main/docker-compose.yml" -o docker-compose.yml
|
sudo curl -L "https://raw.githubusercontent.com/WongSaang/chatgpt-ui/main/docker-compose.yml" -o docker-compose.yml
|
||||||
|
|
||||||
|
echo "Pulling images..."
|
||||||
|
|
||||||
|
sudo docker-compose pull
|
||||||
|
|
||||||
echo "Starting services..."
|
echo "Starting services..."
|
||||||
|
|
||||||
sudo APP_DOMAIN="${APP_DOMAIN}:${SERVER_PORT}" CLIENT_PORT=${CLIENT_PORT} SERVER_PORT=${SERVER_PORT} WSGI_PORT=${WSGI_PORT} docker-compose up --pull always -d
|
sudo APP_DOMAIN="${APP_DOMAIN}:${SERVER_PORT}" CLIENT_PORT=${CLIENT_PORT} SERVER_PORT=${SERVER_PORT} WSGI_PORT=${WSGI_PORT} DB_URL=${DATABASE_URL} docker-compose up -d
|
||||||
|
|
||||||
echo "Done"
|
echo "Done"
|
||||||
17
docker-compose.dev.yml
Normal file
17
docker-compose.dev.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
client:
|
||||||
|
platform: linux/x86_64
|
||||||
|
build: .
|
||||||
|
environment:
|
||||||
|
SERVER_DOMAIN: ${SERVER_DOMAIN:-http://web-server}
|
||||||
|
NUXT_PUBLIC_TYPEWRITER: false
|
||||||
|
ports:
|
||||||
|
- '${CLIENT_PORT:-80}:80'
|
||||||
|
networks:
|
||||||
|
- chatgpt_network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
networks:
|
||||||
|
chatgpt_network:
|
||||||
|
driver: bridge
|
||||||
16
docker-compose.test.yml
Normal file
16
docker-compose.test.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
client:
|
||||||
|
platform: linux/x86_64
|
||||||
|
build: .
|
||||||
|
environment:
|
||||||
|
SERVER_DOMAIN: ${SERVER_DOMAIN:-http://web-server}
|
||||||
|
ports:
|
||||||
|
- '${CLIENT_PORT:-80}:80'
|
||||||
|
networks:
|
||||||
|
- chatgpt_network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
networks:
|
||||||
|
chatgpt_network:
|
||||||
|
driver: bridge
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
version: '3'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
client:
|
client:
|
||||||
|
platform: linux/x86_64
|
||||||
image: wongsaang/chatgpt-ui-client:latest
|
image: wongsaang/chatgpt-ui-client:latest
|
||||||
environment:
|
environment:
|
||||||
- SERVER_DOMAIN=http://backend-web-server
|
- SERVER_DOMAIN=http://backend-web-server
|
||||||
- NUXT_PUBLIC_APP_NAME='ChatGPT UI'
|
- DEFAULT_LOCALE=en
|
||||||
- NUXT_PUBLIC_TYPEWRITER=true
|
# - NUXT_PUBLIC_APP_NAME='ChatGPT UI' # The name of the application
|
||||||
- NUXT_PUBLIC_TYPEWRITER_DELAY=100
|
# - NUXT_PUBLIC_TYPEWRITER=true # Whether to enable the typewriter effect, default false
|
||||||
|
# - NUXT_PUBLIC_TYPEWRITER_DELAY=50 # The delay time of the typewriter effect, default 50ms
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend-web-server
|
- backend-web-server
|
||||||
ports:
|
ports:
|
||||||
@@ -15,10 +17,14 @@ services:
|
|||||||
- chatgpt_ui_network
|
- chatgpt_ui_network
|
||||||
restart: always
|
restart: always
|
||||||
backend-wsgi-server:
|
backend-wsgi-server:
|
||||||
|
platform: linux/x86_64
|
||||||
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||||
environment:
|
environment:
|
||||||
|
- DEBUG=${DEBUG:-False} # Whether to enable debug mode, default False
|
||||||
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000}
|
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000}
|
||||||
# - DB_URL=postgres://postgres:postgrespw@localhost:49153/chatgpt # If this parameter is not set, the built-in Sqlite will be used by default. It should be noted that if you do not connect to an external database, the data will be lost after the container is destroyed.
|
- SERVER_WORKERS=3 # The number of worker processes for handling requests.
|
||||||
|
- WORKER_TIMEOUT=180 # Workers silent for more than this many seconds are killed and restarted. default 180s
|
||||||
|
- DB_URL=${DB_URL:-sqlite:///db.sqlite3} # If this parameter is not set, the built-in Sqlite will be used by default. It should be noted that if you do not connect to an external database, the data will be lost after the container is destroyed.
|
||||||
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name
|
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name
|
||||||
- DJANGO_SUPERUSER_PASSWORD=password # default superuser password
|
- DJANGO_SUPERUSER_PASSWORD=password # default superuser password
|
||||||
- DJANGO_SUPERUSER_EMAIL=admin@example.com # default superuser email
|
- DJANGO_SUPERUSER_EMAIL=admin@example.com # default superuser email
|
||||||
@@ -36,6 +42,7 @@ services:
|
|||||||
- chatgpt_ui_network
|
- chatgpt_ui_network
|
||||||
restart: always
|
restart: always
|
||||||
backend-web-server:
|
backend-web-server:
|
||||||
|
platform: linux/x86_64
|
||||||
image: wongsaang/chatgpt-ui-web-server:latest
|
image: wongsaang/chatgpt-ui-web-server:latest
|
||||||
environment:
|
environment:
|
||||||
- BACKEND_URL=http://backend-wsgi-server:8000
|
- BACKEND_URL=http://backend-wsgi-server:8000
|
||||||
|
|||||||
56
docs/.vuepress/config.ts
Normal file
56
docs/.vuepress/config.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { defineUserConfig, defaultTheme } from 'vuepress'
|
||||||
|
import {
|
||||||
|
navbarEn,
|
||||||
|
navbarZh,
|
||||||
|
sidebarEn,
|
||||||
|
sidebarZh,
|
||||||
|
} from './configs/index.js'
|
||||||
|
|
||||||
|
export default defineUserConfig({
|
||||||
|
title: 'ChatGPT UI',
|
||||||
|
description: 'A ChatGPT web client',
|
||||||
|
base: '/chatgpt-ui/',
|
||||||
|
locales: {
|
||||||
|
// 键名是该语言所属的子路径
|
||||||
|
// 作为特例,默认语言可以使用 '/' 作为其路径。
|
||||||
|
'/': {
|
||||||
|
lang: 'en-US',
|
||||||
|
description: 'A ChatGPT web client',
|
||||||
|
},
|
||||||
|
'/zh/': {
|
||||||
|
lang: 'zh-CN',
|
||||||
|
description: '一个 ChatGPT 的 Web 客户端',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
theme: defaultTheme({
|
||||||
|
locales: {
|
||||||
|
'/': {
|
||||||
|
// navbar
|
||||||
|
navbar: navbarEn,
|
||||||
|
// sidebar
|
||||||
|
sidebar: sidebarEn,
|
||||||
|
},
|
||||||
|
'/zh/': {
|
||||||
|
// navbar
|
||||||
|
navbar: navbarZh,
|
||||||
|
selectLanguageName: '简体中文',
|
||||||
|
selectLanguageText: '选择语言',
|
||||||
|
selectLanguageAriaLabel: '选择语言',
|
||||||
|
// sidebar
|
||||||
|
sidebar: sidebarZh,
|
||||||
|
// 404 page
|
||||||
|
notFound: [
|
||||||
|
'这里什么都没有',
|
||||||
|
'我们怎么到这来了?',
|
||||||
|
'这是一个 404 页面',
|
||||||
|
'看起来我们进入了错误的链接',
|
||||||
|
],
|
||||||
|
backToHome: '返回首页',
|
||||||
|
// a11y
|
||||||
|
openInNewWindow: '在新窗口打开',
|
||||||
|
toggleColorMode: '切换颜色模式',
|
||||||
|
toggleSidebar: '切换侧边栏',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
2
docs/.vuepress/configs/index.ts
Normal file
2
docs/.vuepress/configs/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './navbar/index.js'
|
||||||
|
export * from './sidebar/index.js'
|
||||||
12
docs/.vuepress/configs/navbar/en.ts
Normal file
12
docs/.vuepress/configs/navbar/en.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { NavbarConfig } from '@vuepress/theme-default'
|
||||||
|
|
||||||
|
export const navbarEn: NavbarConfig = [
|
||||||
|
{
|
||||||
|
text: 'Guide',
|
||||||
|
link: '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Changelog',
|
||||||
|
link: 'https://github.com/WongSaang/chatgpt-ui/releases'
|
||||||
|
}
|
||||||
|
]
|
||||||
2
docs/.vuepress/configs/navbar/index.ts
Normal file
2
docs/.vuepress/configs/navbar/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './en.js'
|
||||||
|
export * from './zh.js'
|
||||||
12
docs/.vuepress/configs/navbar/zh.ts
Normal file
12
docs/.vuepress/configs/navbar/zh.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { NavbarConfig } from '@vuepress/theme-default'
|
||||||
|
|
||||||
|
export const navbarZh: NavbarConfig = [
|
||||||
|
{
|
||||||
|
text: '指南',
|
||||||
|
link: '/zh/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '更新日志',
|
||||||
|
link: 'https://github.com/WongSaang/chatgpt-ui/releases',
|
||||||
|
}
|
||||||
|
]
|
||||||
17
docs/.vuepress/configs/sidebar/en.ts
Normal file
17
docs/.vuepress/configs/sidebar/en.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { SidebarConfig } from '@vuepress/theme-default'
|
||||||
|
|
||||||
|
export const sidebarEn: SidebarConfig = {
|
||||||
|
'/': [
|
||||||
|
{
|
||||||
|
text: 'Guide',
|
||||||
|
children: [
|
||||||
|
'/README.md',
|
||||||
|
'/guide/quick-start.md',
|
||||||
|
'/guide/configuration.md',
|
||||||
|
'/guide/problems.md',
|
||||||
|
'/guide/development.md',
|
||||||
|
'/guide/buymeacoffee.md',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
2
docs/.vuepress/configs/sidebar/index.ts
Normal file
2
docs/.vuepress/configs/sidebar/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './en.js'
|
||||||
|
export * from './zh.js'
|
||||||
17
docs/.vuepress/configs/sidebar/zh.ts
Normal file
17
docs/.vuepress/configs/sidebar/zh.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { SidebarConfig } from '@vuepress/theme-default'
|
||||||
|
|
||||||
|
export const sidebarZh: SidebarConfig = {
|
||||||
|
'/zh/': [
|
||||||
|
{
|
||||||
|
text: '指南',
|
||||||
|
children: [
|
||||||
|
'/zh/README.md',
|
||||||
|
'/zh/guide/quick-start.md',
|
||||||
|
'/zh/guide/configuration.md',
|
||||||
|
'/zh/guide/problems.md',
|
||||||
|
'/zh/guide/development.md',
|
||||||
|
'/zh/guide/buymeacoffee.md',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
docs/.vuepress/public/images/bmc_qr.png
Normal file
BIN
docs/.vuepress/public/images/bmc_qr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
43
docs/README.md
Normal file
43
docs/README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
|
||||||
|
# Introduction
|
||||||
|
|
||||||
|
ChatGPT UI is an unofficial ChatGPT web client. It supports multiple users, multiple languages, and multiple database connections for persistent data storage, such as Mysql, PostgreSQL, and Sqlite.
|
||||||
|
|
||||||
|
This project consists of two parts, the client-side and the server-side:
|
||||||
|
|
||||||
|
- Client-side, based on [Nuxt](https://nuxt.com/), project address: [https://github.com/WongSaang/chatgpt-ui](https://github.com/WongSaang/chatgpt-ui)
|
||||||
|
- Server-side, based on [Django](https://djangoproject.com/), project address: [https://github.com/WongSaang/chatgpt-ui-server](https://github.com/WongSaang/chatgpt-ui-server)
|
||||||
|
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Client-side
|
||||||
|
- User system, supporting user registration, login, password modification, and more.
|
||||||
|
- Multi-language user interface, supporting multiple languages.
|
||||||
|
- Persistent data storage, supporting Mysql, PostgreSQL, and Sqlite databases.
|
||||||
|
- Asynchronous conversation, supporting multiple conversations simultaneously.
|
||||||
|
- Management of historical conversations.
|
||||||
|
- Continuous chat, allowing ChatGPT clients to answer questions based on their historical chat records, resulting in better answers.
|
||||||
|
- Web search capability, allowing ChatGPT to retrieve the latest information.
|
||||||
|
- Convenient tools, supporting one-click message and code block copying, as well as message editing.
|
||||||
|
- Common command management, allowing users to store and edit their own common commands.
|
||||||
|
- PWA, supporting installation to the desktop.
|
||||||
|
- User Token Usage Statistics.
|
||||||
|
- Supports configuring multiple API Keys.
|
||||||
|
|
||||||
|
### Server-side
|
||||||
|
- The server-side has an administrative panel.
|
||||||
|
- User management.
|
||||||
|
- Conversation and message management.
|
||||||
|
- Common configurations.
|
||||||
|
|
||||||
|
|
||||||
|
## Original Intention
|
||||||
|
|
||||||
|
Since using ChatGPT, it has become a good helper in work. Unfortunately, as we all know, it cannot be accessed in some places. But fortunately, OpenAI has opened up its API, so I started to write a user interface for myself.
|
||||||
|
|
||||||
|
> Nothing is difficult if you put your heart into it.
|
||||||
|
|
||||||
|
Later, several friends asked me how to use ChatGPT because they didn't have the technical skills. So I started to develop a multi-user system, which can not only be used by myself but also help my family and friends around me.
|
||||||
|
|
||||||
|
After the project was open-sourced, many people raised issues and some even submitted PRs, and the project has developed to its current state. I also learned a lot during this process, as I have always believed that helping others is also helping oneself.
|
||||||
7
docs/guide/buymeacoffee.md
Normal file
7
docs/guide/buymeacoffee.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Donation
|
||||||
|
|
||||||
|
> If this project is helpful to you, it is also helping me.
|
||||||
|
|
||||||
|
If you want to support me, Buy me a coffee ❤️ [https://www.buymeacoffee.com/WongSaang](https://www.buymeacoffee.com/WongSaang)
|
||||||
|
|
||||||
|

|
||||||
84
docs/guide/configuration.md
Normal file
84
docs/guide/configuration.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Configuration Reference
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
By default, the backend uses the built-in Sqlite to store data. If an external database is not connected, the data will be lost after the container is destroyed.
|
||||||
|
|
||||||
|
The `chatgpt-ui-wsgi-server` image provides the environment variable `DB_URL` to configure the connection to an external database. The following table shows the link format of the `DB_URL`.
|
||||||
|
|
||||||
|
| DB | LINK |
|
||||||
|
|----------------------|--------------------------------------------------|
|
||||||
|
| PostgreSQL | postgres://USER:PASSWORD@HOST:PORT/DATABASE_NAME |
|
||||||
|
| MySQL | mysql://USER:PASSWORD@HOST:PORT/DATABASE_NAME |
|
||||||
|
| SQLite | sqlite:///PATH |
|
||||||
|
|
||||||
|
For example, if I am using PostgreSQL, the configuration is as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
backend-wsgi-server:
|
||||||
|
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||||
|
environment:
|
||||||
|
- DB_URL=postgres://postgres:postgrespw@localhost:49153/chatgpt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Email verification
|
||||||
|
|
||||||
|
If you open the user registration feature and need to send email activation links to users, you need to configure the following environment variables in the `wsgi-server` service:
|
||||||
|
|
||||||
|
| Parameters | Description | Default |
|
||||||
|
|----------------------|--------------------------------------------------|-----|
|
||||||
|
| ACCOUNT_EMAIL_VERIFICATION | E-mail authentication method, optional value: none, optional, mandatory | optional |
|
||||||
|
| EMAIL_HOST | SMTP server address | smtp.mailgun.org |
|
||||||
|
| EMAIL_PORT | SMTP server port | 587 |
|
||||||
|
| EMAIL_HOST_USER | User name | - |
|
||||||
|
| EMAIL_HOST_PASSWORD | Password | - |
|
||||||
|
| EMAIL_USE_TLS | Whether to encrypt | True |
|
||||||
|
| EMAIL_FROM | From email | webmaster@localhost |
|
||||||
|
|
||||||
|
## API Proxy
|
||||||
|
|
||||||
|
If you are unable to request the OpenAI API address due to network restrictions, you can configure a proxy in the `wsgi-server` service. You will need to search for how to set up a proxy server on your own.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
backend-wsgi-server:
|
||||||
|
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||||
|
environment:
|
||||||
|
- OPENAI_API_PROXY=https://openai.proxy.com/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend CSRF whitelist
|
||||||
|
|
||||||
|
If you encounter `CSRF verification failed` while accessing the management background, your `APP_DOMAIN` may not be configured correctly. Under the `wsgi-server` service, there is an environment variable `wsgi-server`. Its value should be the address and port of `backend-web-server`, default: `localhost:9000`.
|
||||||
|
|
||||||
|
Suppose I have resolved the domain name `chagpt.com` to the server, and my `backend-web-server` service is bound to port 9000. The correct configuration is as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
backend-wsgi-server:
|
||||||
|
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||||
|
environment:
|
||||||
|
- APP_DOMAIN=chagpt.com:9000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client Configuration
|
||||||
|
|
||||||
|
| Parameter | Description | Default Value |
|
||||||
|
|-----------------------|---------------------------------------------|----------------------------|
|
||||||
|
| SERVER_DOMAIN | Server Address | http://backend-web-server |
|
||||||
|
| DEFAULT_LOCALE | Default Language | en |
|
||||||
|
| NUXT_PUBLIC_APP_NAME | Application Name | ChatGPT UI |
|
||||||
|
| NUXT_PUBLIC_TYPEWRITER| Enable Typewriter Effect [true/false] | true |
|
||||||
|
| NUXT_PUBLIC_TYPEWRITER_DELAY | Typewriter Effect Delay in milliseconds | 50 |
|
||||||
|
|
||||||
|
## User Registration Control
|
||||||
|
|
||||||
|
After deployment, there is an `open_registration` setting under `Chat->Settings` in the admin panel to control whether user registration is allowed. The default value is `True` (allowing user registration). If not needed, please change it to `False`.
|
||||||
|
|
||||||
|
## Web Search Function Control
|
||||||
|
|
||||||
|
This feature is disabled by default. You can enable it in the admin panel under `Chat->Settings`. There is a setting called `open_web_search`, set its value to `True`.
|
||||||
|
|
||||||
|
## Frugal Mode Control
|
||||||
|
|
||||||
|
This feature is enabled by default. You can disable it in the `Chat->Settings` section of the management backend. There is a setting called `open_frugal_mode_control` in Settings. Set its value to `False`.
|
||||||
62
docs/guide/development.md
Normal file
62
docs/guide/development.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Development Guide
|
||||||
|
|
||||||
|
## Front-end
|
||||||
|
|
||||||
|
Required skills: [Vue](https://vuejs.org/), [Nuxt](https://nuxt.com/)
|
||||||
|
|
||||||
|
Project address: [https://github.com/WongSaang/chatgpt-ui](https://github.com/WongSaang/chatgpt-ui)
|
||||||
|
|
||||||
|
### Environment Setup
|
||||||
|
Install the latest stable version of node.js. If you need to package it as a docker image, you also need to install docker.
|
||||||
|
|
||||||
|
### Install dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start development server
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Package as a docker image
|
||||||
|
|
||||||
|
```
|
||||||
|
docker build -t image-name:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Back-end
|
||||||
|
|
||||||
|
Required skills: [Python](https://www.python.org/), [Django](https://djangoproject.com/)
|
||||||
|
|
||||||
|
Project address: [https://github.com/WongSaang/chatgpt-ui-server](https://github.com/WongSaang/chatgpt-ui-server)
|
||||||
|
|
||||||
|
### Environment Setup
|
||||||
|
Install Python, pip/pipenv. If you need to package it as a docker image, you also need to install docker.
|
||||||
|
|
||||||
|
### Install dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start development server
|
||||||
|
|
||||||
|
```
|
||||||
|
python manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
### Package as a docker image
|
||||||
|
|
||||||
|
```
|
||||||
|
docker build -t image-name:latest .
|
||||||
|
```
|
||||||
13
docs/guide/problems.md
Normal file
13
docs/guide/problems.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Encountering Issues
|
||||||
|
|
||||||
|
## Searching for Issues
|
||||||
|
|
||||||
|
If you encounter any issues while using the project, you can search for related keywords on the project's [Issues](https://github.com/WongSaang/chatgpt-ui/issues) page to see if others have faced similar issues and if there are any solutions available.
|
||||||
|
|
||||||
|
## Submitting an Issue
|
||||||
|
|
||||||
|
If you cannot find a solution, you can communicate with the project maintainers by submitting an issue. [Submit an Issue](https://github.com/WongSaang/chatgpt-ui/issues/new)
|
||||||
|
|
||||||
|
**Note**
|
||||||
|
|
||||||
|
The title should be clear and concise, and the description should provide as much detail as possible about the issue or suggestion. If possible, it is best to provide reproducible steps and screenshots.
|
||||||
117
docs/guide/quick-start.md
Normal file
117
docs/guide/quick-start.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Quick Start
|
||||||
|
|
||||||
|
This project provides related docker images for deployment on a VPS or your local computer. Please note that if your network is unable to request the OpenAI API address, you need to configure a proxy. If you want to make it available to other users, it's best to have a domain name and resolve it to the server.
|
||||||
|
|
||||||
|
You also need an OpenAI API Key, and there are multiple ways to obtain it online, please search for it yourself.
|
||||||
|
|
||||||
|
## Deploying
|
||||||
|
|
||||||
|
### Quickly deploy script
|
||||||
|
|
||||||
|
**Note: This script has only been verified on Ubuntu Server 22.04 LTS.**
|
||||||
|
|
||||||
|
```
|
||||||
|
bash <(curl -Ls https://raw.githubusercontent.com/WongSaang/chatgpt-ui/main/deployment.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
|
||||||
|
#### Prepare docker-compose.yml
|
||||||
|
|
||||||
|
The project provides a sample `docker-compose.yml`. If you want to customize the configuration, please refer to the [configuration reference](/en/guide/configuration) section.
|
||||||
|
|
||||||
|
You can download the `docker-compose.yml` template to your local machine or server by clicking on the link below:
|
||||||
|
|
||||||
|
[https://raw.githubusercontent.com/WongSaang/chatgpt-ui/main/docker-compose.yml](https://raw.githubusercontent.com/WongSaang/chatgpt-ui/main/docker-compose.yml)
|
||||||
|
|
||||||
|
You can also manually create the `docker-compose.yml` file and copy the following content into the file:
|
||||||
|
|
||||||
|
```
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
client:
|
||||||
|
platform: linux/x86_64
|
||||||
|
image: wongsaang/chatgpt-ui-client:latest
|
||||||
|
environment:
|
||||||
|
- SERVER_DOMAIN=http://backend-web-server
|
||||||
|
- DEFAULT_LOCALE=en
|
||||||
|
# - NUXT_PUBLIC_APP_NAME='ChatGPT UI' # The name of the application
|
||||||
|
# - NUXT_PUBLIC_TYPEWRITER=true # Whether to enable the typewriter effect, default false
|
||||||
|
# - NUXT_PUBLIC_TYPEWRITER_DELAY=50 # The delay time of the typewriter effect, default 50ms
|
||||||
|
depends_on:
|
||||||
|
- backend-web-server
|
||||||
|
ports:
|
||||||
|
- '${CLIENT_PORT:-80}:80'
|
||||||
|
networks:
|
||||||
|
- chatgpt_ui_network
|
||||||
|
restart: always
|
||||||
|
backend-wsgi-server:
|
||||||
|
platform: linux/x86_64
|
||||||
|
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||||
|
environment:
|
||||||
|
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000}
|
||||||
|
- SERVER_WORKERS=3 # The number of worker processes for handling requests.
|
||||||
|
# - DB_URL=postgres://postgres:postgrespw@localhost:49153/chatgpt # If this parameter is not set, the built-in Sqlite will be used by default. It should be noted that if you do not connect to an external database, the data will be lost after the container is destroyed.
|
||||||
|
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name
|
||||||
|
- DJANGO_SUPERUSER_PASSWORD=password # default superuser password
|
||||||
|
- DJANGO_SUPERUSER_EMAIL=admin@example.com # default superuser email
|
||||||
|
- ACCOUNT_EMAIL_VERIFICATION=${ACCOUNT_EMAIL_VERIFICATION:-none} # Determines the e-mail verification method during signup – choose one of "none", "optional", or "mandatory". Default is "optional". If you don't need to verify the email, you can set it to "none".
|
||||||
|
# If you want to use the email verification function, you need to configure the following parameters
|
||||||
|
# - EMAIL_HOST=SMTP server address
|
||||||
|
# - EMAIL_PORT=SMTP server port
|
||||||
|
# - EMAIL_HOST_USER=
|
||||||
|
# - EMAIL_HOST_PASSWORD=
|
||||||
|
# - EMAIL_USE_TLS=True
|
||||||
|
# - EMAIL_FROM=no-reply@example.com #Default sender email address
|
||||||
|
ports:
|
||||||
|
- '${WSGI_PORT:-8000}:8000'
|
||||||
|
networks:
|
||||||
|
- chatgpt_ui_network
|
||||||
|
restart: always
|
||||||
|
backend-web-server:
|
||||||
|
platform: linux/x86_64
|
||||||
|
image: wongsaang/chatgpt-ui-web-server:latest
|
||||||
|
environment:
|
||||||
|
- BACKEND_URL=http://backend-wsgi-server:8000
|
||||||
|
ports:
|
||||||
|
- '${SERVER_PORT:-9000}:80'
|
||||||
|
depends_on:
|
||||||
|
- backend-wsgi-server
|
||||||
|
networks:
|
||||||
|
- chatgpt_ui_network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
networks:
|
||||||
|
chatgpt_ui_network:
|
||||||
|
driver: bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Starting the Service
|
||||||
|
|
||||||
|
After modifying the configuration as needed, you can start the service by running the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose up --pull always -d
|
||||||
|
```
|
||||||
|
|
||||||
|
This command is used to start the services specified in the Docker Compose configuration. The specific meanings of the parameters are as follows:
|
||||||
|
|
||||||
|
- `up`: start the services specified in the Docker Compose configuration.
|
||||||
|
- `--pull always`: before starting the service each time, the latest version of the image will be pulled from the Docker image repository. This ensures that the image used is always up to date.
|
||||||
|
- `-d`: run the service in the background. If this parameter is not added, the service will run in the current terminal window until the user manually stops it.
|
||||||
|
|
||||||
|
## After Deployment
|
||||||
|
|
||||||
|
Access the management panel at `http(s)://your.domain:9000/admin` or `http(s)://123.123.123.123:9000/admin` using the default superuser account:
|
||||||
|
|
||||||
|
- username: **admin**
|
||||||
|
- password: **password**
|
||||||
|
|
||||||
|
~~Before starting a chat, you need to add an OpenAI API key. In the management panel, in the "Settings" section, there is a record named `openai_api_key`. Set the value to your API key.~~
|
||||||
|
|
||||||
|
In the latest version, a separate API Key management has been added to the admin panel, located under "Provider/Api keys". You can add multiple API Keys here, and the backend program will track the usage of each key's token and balance the usage based on token usage. **To enable this feature, you need to delete the previous "openai_api_key" setting.**
|
||||||
|
|
||||||
|
Now you can access the client at `http(s)://your.domain` or `http://123.123.123.123` to start chatting.
|
||||||
|
|
||||||
|
🎉🎉🎉 Have fun!
|
||||||
@@ -1,200 +1,42 @@
|
|||||||
<div align="center">
|
# 介绍
|
||||||
<h1>ChatGPT UI</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
[English](../../README.md) | [中文](./docs/zh/README.md)
|
ChatGPT UI 是一个非官方的 ChatGPT Web 客户端。它支持多用户,多语言,多种数据库连接进行数据持久化存储,例如:Mysql、PostgreSQL 和 Sqlite 等。
|
||||||
|
|
||||||
ChatGPT Web 客户端,支持多用户,支持 Mysql、PostgreSQL 等多种数据库连接进行数据持久化存储,支持多语言。提供 Docker 镜像和快速部署脚本。
|
本项目项目包括客户端和服务端两部分。
|
||||||
|
|
||||||
https://user-images.githubusercontent.com/46235412/227156264-ca17ab17-999b-414f-ab06-3f75b5235bfe.mp4
|
- 客户端,基于 [Nuxt](https://nuxt.com/),项目地址:[https://github.com/WongSaang/chatgpt-ui](https://github.com/WongSaang/chatgpt-ui)
|
||||||
|
- 服务端,基于 [Django](https://djangoproject.com/),项目地址:[https://github.com/WongSaang/chatgpt-ui-server](https://github.com/WongSaang/chatgpt-ui-server)
|
||||||
|
|
||||||
|
|
||||||
## 📢 更新
|
## 功能与特性
|
||||||
<details open>
|
|
||||||
<summary><strong>2023-03-23</strong></summary>
|
|
||||||
增加网页搜索能力,使得 ChatGPT 生成的回答更与时俱进!
|
|
||||||
该功能默认处于关闭状态,你可以在管理后台的 `Chat->Settings` 中开启它,在 Settings 中有一个 `open_web_search` 的记录,把它的值设置为 True。
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open>
|
### 客户端
|
||||||
<summary><strong>2023-03-15</strong></summary>
|
- 用户系统,支持用户注册、登录、修改密码等。
|
||||||
|
- 用户界面多语言,支持多种语言。
|
||||||
|
- 数据持久化,支持 Mysql、PostgreSQL 和 Sqlite 等数据库。
|
||||||
|
- 异步对话,支持多个对话同时进行。
|
||||||
|
- 历史对话管理。
|
||||||
|
- 持续聊天,让 ChatGPT 客户历史聊天记录回答问题,得出更好的答案。
|
||||||
|
- 网页搜索能力,让 ChatGPT 获取最新信息。
|
||||||
|
- 便捷的工具,支持一键复制消息和代码块,以及重新编辑消息等。
|
||||||
|
- 常用指令管理,用户可存储和编辑自己的常用指令。
|
||||||
|
- PWA,支持安装到桌面。
|
||||||
|
- 用户 Token 使用量统计
|
||||||
|
- 支持配置多个 API Key
|
||||||
|
|
||||||
在管理后台增加 `open_registration` 设置项,用于控制是否开放用户注册。你可以登录管理后台,在 `Chat->Setting` 中看到这个设置项,默认是 `True` (允许用户注册),如果不需要,请改成 `False`。
|
### 服务端
|
||||||
|
- 服务端拥有一个管理面板
|
||||||
</details>
|
- 用户管理
|
||||||
|
- 对话和消息管理
|
||||||
<details open>
|
- 常用配置
|
||||||
<summary><strong>2023-03-10</strong></summary>
|
|
||||||
|
|
||||||
增加 2 个环境变量来控制打字机效果, 详见下方 docker-compose 配置的环境变量说明
|
|
||||||
|
|
||||||
- `NUXT_PUBLIC_TYPEWRITER` 是否开启打字机效果
|
|
||||||
- `NUXT_PUBLIC_TYPEWRITER_DELAY` 每个字的延迟时间,单位:毫秒
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details open>
|
|
||||||
<summary><strong>2023-03-04</strong></summary>
|
|
||||||
|
|
||||||
**使用最新的官方聊天模型** `gpt-3.5-turbo`
|
|
||||||
|
|
||||||
**🎉🎉🎉 提供一个 shell 脚本,用于快速部署到服务器** [使用方法](#one-click-depolyment)
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
|
|
||||||
<summary><strong>2023-02-24</strong></summary>
|
|
||||||
V2 是一个重要的更新,将后端功能分离为一个独立的项目,托管在 [chatgpt-ui-server](https://github.com/WongSaang/chatgpt-ui-server), 该项目使用基于 Python 的 Django 框架。
|
|
||||||
|
|
||||||
如果您仍然希望使用旧版本,请访问 [v1 branch](https://github.com/WongSaang/chatgpt-ui/tree/v1) (不推荐,不再更新).
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## V2 的功能特性:
|
|
||||||
|
|
||||||
- 😉 前后端分离,后端使用基于 Python 的 Django 框架。
|
|
||||||
- 😘 用户身份验证,支持多个用户。
|
|
||||||
- 😀 能够将数据存储在外部数据库中,支持 Mysql、PostgreSQL 等数据库(默认为 Sqlite)。
|
|
||||||
- 😎 持续对话,让AI根据上下文回答问题。
|
|
||||||
|
|
||||||
|
|
||||||
## 🚀 一行命令部署到服务器 <a name="one-click-depolyment"></a>
|
## 初衷
|
||||||
|
|
||||||
注意:此脚本仅在 Ubuntu Server 22.04 LTS 上验证过。
|
自从使用 ChatGPT ,它已经成为工作中的好帮手。可惜的是,就像大家知道的,它在有些地方无法访问。但好在 OpenAI 开放了 API,于是我开始为自己写用户界面。
|
||||||
|
|
||||||
```bash
|
> 世上无难事,只怕有心人。
|
||||||
bash <(curl -Ls https://raw.githubusercontent.com/WongSaang/chatgpt-ui/main/deployment.sh)
|
|
||||||
```
|
|
||||||
|
|
||||||
> 如果您拥有一个域名,可以使用 DNS 解析将其指向服务器的 IP 地址。当然,直接使用服务器的 IP 地址也是可以的。
|
后来,有多位朋友询问我怎么样才能使用 ChatGPT,因为他们没有技术能力。于是我又着手于多用户系统的开发,这样除了自己用,还能帮助到身边的亲朋好友。
|
||||||
> 在脚本执行期间,会提示您输入域名。如果您没有域名,可以直接输入服务器的 IP 地址。
|
|
||||||
|
|
||||||
### 部署完成之后
|
项目开源后,有很多人提了 issue,也有人提了 PR,项目就发展到如今的样子。我在这个过程中也学到了很多,正如我一直坚信的,帮助他人也是帮助自己。
|
||||||
|
|
||||||
访问 `http(s)://your.domain:9000/admin` / IP `http(s)://123.123.123.123:9000/admin` 登录管理面板。
|
|
||||||
|
|
||||||
默认超级用户: `admin`
|
|
||||||
|
|
||||||
默认密码: `password`
|
|
||||||
|
|
||||||
在可以开始聊天之前,您需要添加一个 OpenAI 的 API 密钥。在管理面板的设置模型中,添加一个名称为 openai_api_key 的记录,将值设置为您的 API 密钥。
|
|
||||||
|
|
||||||
现在可以访问客户端地址 `http(s)://your.domain` / `http://123.123.123.123` 开始聊天。
|
|
||||||
|
|
||||||
🎉🎉🎉 享受吧!
|
|
||||||
|
|
||||||
## 通过 Docker Compose 快速开始
|
|
||||||
|
|
||||||
以下是一个 docker-compose.yml 模板,您可以使用它来快速启动服务。
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3'
|
|
||||||
services:
|
|
||||||
client:
|
|
||||||
image: wongsaang/chatgpt-ui-client:latest
|
|
||||||
environment:
|
|
||||||
- SERVER_DOMAIN=http://backend-web-server
|
|
||||||
- NUXT_PUBLIC_APP_NAME='ChatGPT UI' # App 名称,默认为 ChatGPT UI
|
|
||||||
- NUXT_PUBLIC_TYPEWRITER=true # 是否启用打字机效果,默认关闭
|
|
||||||
- NUXT_PUBLIC_TYPEWRITER_DELAY=100 # 打字机效果的延迟时间,默认 50毫秒
|
|
||||||
depends_on:
|
|
||||||
- backend-web-server
|
|
||||||
ports:
|
|
||||||
- '80:80'
|
|
||||||
networks:
|
|
||||||
- chatgpt_ui_network
|
|
||||||
backend-wsgi-server:
|
|
||||||
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
|
||||||
environment:
|
|
||||||
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000} # CSRF 白名单,在这里设置为 chatgpt-ui-web-server 的地址+端口, 默认: localhost:9000
|
|
||||||
#- DB_URL=postgres://postgres:postgrespw@localhost:49153/chatgpt # 连接外部数据库,如果不设置这个参数,则默认使用内置的 Sqlite。需要注意的是,如果不连接外部数据库,数据将在容器销毁后丢失。链接格式请看下面的 DB_URL 格式对照表
|
|
||||||
#- OPENAI_API_PROXY=https://openai.proxy.com/v1 # https://api.openai.com/v1 的代理地址
|
|
||||||
- DJANGO_SUPERUSER_USERNAME=admin # 默认超级用户
|
|
||||||
- DJANGO_SUPERUSER_PASSWORD=password # 默认超级用户的密码
|
|
||||||
- DJANGO_SUPERUSER_EMAIL=admin@example.com # 默认超级用户邮箱
|
|
||||||
- ACCOUNT_EMAIL_VERIFICATION=none # 邮箱验证方式,可选值: none, optional, mandatory. 默认为 optional。如果你不需要验证用户的邮箱,可以设置为 none。
|
|
||||||
# 如果您想使用电子邮件验证功能,需要配置以下参数:
|
|
||||||
# - EMAIL_HOST=SMTP server address
|
|
||||||
# - EMAIL_PORT=SMTP server port
|
|
||||||
# - EMAIL_HOST_USER=
|
|
||||||
# - EMAIL_HOST_PASSWORD=
|
|
||||||
# - EMAIL_USE_TLS=True
|
|
||||||
# - EMAIL_FROM=no-reply@example.com #默认发件邮箱地址
|
|
||||||
ports:
|
|
||||||
- '8000:8000'
|
|
||||||
networks:
|
|
||||||
- chatgpt_ui_network
|
|
||||||
backend-web-server:
|
|
||||||
image: wongsaang/chatgpt-ui-web-server:latest
|
|
||||||
environment:
|
|
||||||
- BACKEND_URL=http://backend-wsgi-server:8000
|
|
||||||
ports:
|
|
||||||
- '9000:80'
|
|
||||||
depends_on:
|
|
||||||
- backend-wsgi-server
|
|
||||||
networks:
|
|
||||||
- chatgpt_ui_network
|
|
||||||
|
|
||||||
networks:
|
|
||||||
chatgpt_ui_network:
|
|
||||||
driver: bridge
|
|
||||||
```
|
|
||||||
|
|
||||||
### DB_URL 格式对照表
|
|
||||||
|
|
||||||
| 数据库 | 链接 |
|
|
||||||
|----------------------|--------------------------------------------------|
|
|
||||||
| PostgreSQL | ``postgres://USER:PASSWORD@HOST:PORT/NAME`` |
|
|
||||||
| MySQL | ``mysql://USER:PASSWORD@HOST:PORT/NAME`` |
|
|
||||||
| SQLite | ``sqlite:///PATH`` |
|
|
||||||
|
|
||||||
### 设置 API 密钥
|
|
||||||
|
|
||||||
访问 `http(s)://your.domain:9000/admin` / IP `http(s)://123.123.123.123:9000/admin` 登录管理面板。
|
|
||||||
|
|
||||||
默认超级用户: `admin`
|
|
||||||
|
|
||||||
默认密码: `password`
|
|
||||||
|
|
||||||
在可以开始聊天之前,您需要添加一个 OpenAI 的 API 密钥。在管理面板的设置模型中,添加一个名称为 openai_api_key 的记录,将值设置为您的 API 密钥。
|
|
||||||
|
|
||||||
现在可以访问客户端地址 `http(s)://your.domain` / `http://123.123.123.123` 开始聊天。
|
|
||||||
|
|
||||||
|
|
||||||
## 续杯咖啡
|
|
||||||
|
|
||||||
> 如果对您有帮助,也是在帮助我自己.
|
|
||||||
|
|
||||||
如果你想支持我,给我续杯咖啡吧 ❤️ [https://www.buymeacoffee.com/WongSaang](https://www.buymeacoffee.com/WongSaang)
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img height="150" src="https://github.com/WongSaang/chatgpt-ui/blob/main/demos/bmc_qr.png?raw=true"/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Setup
|
|
||||||
|
|
||||||
Make sure to install the dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# yarn
|
|
||||||
yarn install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development Server
|
|
||||||
|
|
||||||
Start the development server on http://localhost:3000
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production
|
|
||||||
|
|
||||||
Build the application for production:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn build
|
|
||||||
```
|
|
||||||
7
docs/zh/guide/buymeacoffee.md
Normal file
7
docs/zh/guide/buymeacoffee.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# 续杯咖啡
|
||||||
|
|
||||||
|
> 如果这个项目对您有帮助,这也是在帮助我自己。
|
||||||
|
|
||||||
|
如果你想支持我,给我续杯咖啡吧 ❤️ [https://www.buymeacoffee.com/WongSaang](https://www.buymeacoffee.com/WongSaang)
|
||||||
|
|
||||||
|

|
||||||
85
docs/zh/guide/configuration.md
Normal file
85
docs/zh/guide/configuration.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# 配置参考
|
||||||
|
|
||||||
|
## 数据库
|
||||||
|
|
||||||
|
后端默认使用内置的 Sqlite 来存储数据,如果不连接外部数据库,数据将在容器销毁后丢失。
|
||||||
|
|
||||||
|
`chatgpt-ui-wsgi-server` 镜像提供环境变量 `DB_URL` 来配置与外部数据库的连接,以下是 `DB_URL` 的链接格式对照表。
|
||||||
|
|
||||||
|
| 数据库 | 链接 |
|
||||||
|
|----------------------|--------------------------------------------------|
|
||||||
|
| PostgreSQL | postgres://USER:PASSWORD@HOST:PORT/DATABASE_NAME |
|
||||||
|
| MySQL | mysql://USER:PASSWORD@HOST:PORT/DATABASE_NAME |
|
||||||
|
| SQLite | sqlite:///PATH |
|
||||||
|
|
||||||
|
例如我使用 PostgreSQL,则配置如下:
|
||||||
|
|
||||||
|
```
|
||||||
|
backend-wsgi-server:
|
||||||
|
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||||
|
environment:
|
||||||
|
- DB_URL=postgres://postgres:postgrespw@localhost:49153/chatgpt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 邮箱验证
|
||||||
|
|
||||||
|
如果你开放用户注册功能,并需要向用户发送邮箱激活链接,需要在 `wsgi-server` 服务中配置以下环境变量:
|
||||||
|
|
||||||
|
| 参数 | 说明 | 默认值 |
|
||||||
|
|----------------------|--------------------------------------------------|-----|
|
||||||
|
| ACCOUNT_EMAIL_VERIFICATION | 邮箱验证方式,可选值: none, optional, mandatory | optional |
|
||||||
|
| EMAIL_HOST | SMTP 服务器地址 | smtp.mailgun.org |
|
||||||
|
| EMAIL_PORT | SMTP 服务器端口号 | 587 |
|
||||||
|
| EMAIL_HOST_USER | 用户名 | - |
|
||||||
|
| EMAIL_HOST_PASSWORD | 密码 | - |
|
||||||
|
| EMAIL_USE_TLS | 是否加密 | True |
|
||||||
|
| EMAIL_FROM | 发件邮箱 | webmaster@localhost |
|
||||||
|
|
||||||
|
## API 代理
|
||||||
|
|
||||||
|
如果您的网络无法请求 OpenAI 的 API 地址,您可以在 `wsgi-server` 服务中配置代理,如何搭建代理服务,需要您自行搜索。
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
```
|
||||||
|
backend-wsgi-server:
|
||||||
|
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||||
|
environment:
|
||||||
|
- OPENAI_API_PROXY=https://openai.proxy.com/v1 # 注意,域名后面需要带上 v1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 后端 CSRF 白名单
|
||||||
|
|
||||||
|
如果你在访问管理后台的时候遇到 `CSRF verification failed`,可能你的 `APP_DOMAIN` 没有配置对。在 `wsgi-server` 服务下有个环境变量 `wsgi-server`。 它的值应该是 `backend-web-server` 的地址+端口, 默认: `localhost:9000`。
|
||||||
|
|
||||||
|
假如我把 `chagpt.com` 这个域名解析到了服务器,并且我的 `backend-web-server` 服务绑定了 9000 这个端口。正确的配置如下:
|
||||||
|
|
||||||
|
```
|
||||||
|
backend-wsgi-server:
|
||||||
|
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||||
|
environment:
|
||||||
|
- APP_DOMAIN=chagpt.com:9000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 客户端配置
|
||||||
|
|
||||||
|
| 参数 | 说明 | 默认值 |
|
||||||
|
|----------------------|-------------------------------------------|---------------------------|
|
||||||
|
| SERVER_DOMAIN | 服务端地址 | http://backend-web-server |
|
||||||
|
| DEFAULT_LOCALE | 默认语言 | en |
|
||||||
|
| NUXT_PUBLIC_APP_NAME | 应用名称 | ChatGPT UI |
|
||||||
|
| NUXT_PUBLIC_TYPEWRITER | 是否开启 打字机 效果[true/false]| true |
|
||||||
|
| NUXT_PUBLIC_TYPEWRITER_DELAY | 打字机效果的延迟时间,单位:毫秒| 50 |
|
||||||
|
|
||||||
|
|
||||||
|
## 用户注册控制
|
||||||
|
|
||||||
|
部署完整后,在管理后台的 `Chat->Setting` 下面有 `open_registration` 设置项,用于控制是否开放用户注册。默认是 `True` (允许用户注册),如果不需要,请改成 `False`。
|
||||||
|
|
||||||
|
## 网页搜索功能控制
|
||||||
|
|
||||||
|
该功能默认处于关闭状态,你可以在管理后台的 `Chat->Settings` 中开启它,在 Settings 中有一个 `open_web_search` 的设置项,把它的值设置为 `True`。
|
||||||
|
|
||||||
|
## 节俭模式控制
|
||||||
|
|
||||||
|
该功能默认处于开启状态,你可以在管理后台的 `Chat->Settings` 中关闭它,在 Settings 中有一个 `open_frugal_mode_control` 的设置项,把它的值设置为 `False`。
|
||||||
62
docs/zh/guide/development.md
Normal file
62
docs/zh/guide/development.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# 开发指南
|
||||||
|
|
||||||
|
## 前端
|
||||||
|
|
||||||
|
所需技能:[Vue](https://vuejs.org/)、[Nuxt](https://nuxt.com/)
|
||||||
|
|
||||||
|
项目地址:[https://github.com/WongSaang/chatgpt-ui](https://github.com/WongSaang/chatgpt-ui)
|
||||||
|
|
||||||
|
### 环境准备
|
||||||
|
安装最新稳定版 node.js,如果需要打包成 docker 镜像,还需要安装 docker。
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 启动开发服务
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 打包成 docker 镜像
|
||||||
|
|
||||||
|
```
|
||||||
|
docker build -t image-name:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 后端
|
||||||
|
|
||||||
|
所需技能:[Python](https://www.python.org/)、[Django](https://djangoproject.com/)
|
||||||
|
|
||||||
|
项目地址:[https://github.com/WongSaang/chatgpt-ui-server](https://github.com/WongSaang/chatgpt-ui-server)
|
||||||
|
|
||||||
|
### 环境准备
|
||||||
|
安装Python、pip/pipenv,如果需要打包成 docker 镜像,还需要安装 docker。
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 启动开发服务
|
||||||
|
|
||||||
|
```
|
||||||
|
python manage.py runserver
|
||||||
|
```
|
||||||
|
|
||||||
|
### 打包成 docker 镜像
|
||||||
|
|
||||||
|
```
|
||||||
|
docker build -t image-name:latest .
|
||||||
|
```
|
||||||
13
docs/zh/guide/problems.md
Normal file
13
docs/zh/guide/problems.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 遇到问题
|
||||||
|
|
||||||
|
## 搜索问题
|
||||||
|
|
||||||
|
当你在使用项目时,如果遇到了问题,可以在项目的 [Issues](https://github.com/WongSaang/chatgpt-ui/issues) 页面搜索相关的关键词,看看其他人是否遇到过相同的问题以及解决方案。
|
||||||
|
|
||||||
|
## 提 issue
|
||||||
|
|
||||||
|
如果没有找到解决方案,可以通过提交 Issue 来与项目维护者交流。[提交Issue](https://github.com/WongSaang/chatgpt-ui/issues/new)
|
||||||
|
|
||||||
|
**注意**
|
||||||
|
|
||||||
|
标题应该简单明了,描述应该尽可能详细地描述问题或者建议。如果可能,最好提供复现步骤和截图。
|
||||||
117
docs/zh/guide/quick-start.md
Normal file
117
docs/zh/guide/quick-start.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# 快速开始
|
||||||
|
|
||||||
|
本项目提供了相关的 docker 镜像,你需要一个 vps 来部署,当然你也可以在本地的电脑上部署。需要注意的是,如果你的网络无法请求 OpenAI 的 API 地址,您需要配置代理。如果你想开放给其他用户使用,最好还需要一个域名,并将域名解析到服务器。
|
||||||
|
|
||||||
|
您还需要一个 OpenAI 的API Key,网上有获取多种方案,请自行搜索。
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
### 快速部署脚本
|
||||||
|
|
||||||
|
*对于技术知识了解不多的选手,如果你看不懂下面的内容,可以看我之前写的博客文章[《一行命令部署自己的ChatGPT客户端》](https://wongsnotes.com/p/deploying-your-own-chatgpt-client-with-one-line-of-command/)*
|
||||||
|
|
||||||
|
**注意:此脚本目前仅在 Ubuntu Server 22.04 LTS 上验证过。**
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
bash <(curl -Ls https://raw.githubusercontent.com/WongSaang/chatgpt-ui/main/deployment.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
|
||||||
|
#### 准备 docker-compose.yml
|
||||||
|
|
||||||
|
项目中提供了一个 `docker-compose.yml` 示例,如果你想自定义配置,请看 [配置参考](/zh/guide/configuration) 部分。
|
||||||
|
|
||||||
|
你可以通过下方链接下载 `docker-compose.yml` 模板到本地或服务器:
|
||||||
|
|
||||||
|
[https://raw.githubusercontent.com/WongSaang/chatgpt-ui/main/docker-compose.yml](https://raw.githubusercontent.com/WongSaang/chatgpt-ui/main/docker-compose.yml)
|
||||||
|
|
||||||
|
也可以手动创建 `docker-compose.yml` 文件,然后复制下面的内容到文件中:
|
||||||
|
|
||||||
|
```
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
client:
|
||||||
|
image: wongsaang/chatgpt-ui-client:latest
|
||||||
|
environment:
|
||||||
|
- SERVER_DOMAIN=http://backend-web-server
|
||||||
|
- DEFAULT_LOCALE=zh
|
||||||
|
# - NUXT_PUBLIC_APP_NAME='ChatGPT UI' # APP 名称
|
||||||
|
# - NUXT_PUBLIC_TYPEWRITER=true # 是否开启 打字机 效果
|
||||||
|
# - NUXT_PUBLIC_TYPEWRITER_DELAY=50 # 打字机效果的延迟时间,单位:毫秒,默认:50
|
||||||
|
depends_on:
|
||||||
|
- backend-web-server
|
||||||
|
ports:
|
||||||
|
- '80:80'
|
||||||
|
networks:
|
||||||
|
- chatgpt_ui_network
|
||||||
|
backend-wsgi-server:
|
||||||
|
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||||
|
environment:
|
||||||
|
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000} # CSRF 白名单,在这里设置为 chatgpt-ui-web-server 的地址+端口, 默认: localhost:9000
|
||||||
|
- SERVER_WORKERS=3 # gunicorn 的工作进程数,默认为 3
|
||||||
|
#- DB_URL=postgres://postgres:postgrespw@localhost:49153/chatgpt # 连接外部数据库,如果不设置这个参数,则默认使用内置的 Sqlite。需要注意的是,如果不连接外部数据库,数据将在容器销毁后丢失。链接格式请看下面的 DB_URL 格式对照表
|
||||||
|
#- OPENAI_API_PROXY=https://openai.proxy.com/v1 # https://api.openai.com/v1 的代理地址
|
||||||
|
- DJANGO_SUPERUSER_USERNAME=admin # 默认超级用户
|
||||||
|
- DJANGO_SUPERUSER_PASSWORD=password # 默认超级用户的密码
|
||||||
|
- DJANGO_SUPERUSER_EMAIL=admin@example.com # 默认超级用户邮箱
|
||||||
|
- ACCOUNT_EMAIL_VERIFICATION=none # 邮箱验证方式,可选值: none, optional, mandatory. 默认为 optional。如果你不需要验证用户的邮箱,可以设置为 none。
|
||||||
|
# 如果您想使用电子邮件验证功能,需要配置以下参数:
|
||||||
|
# - EMAIL_HOST=SMTP server address
|
||||||
|
# - EMAIL_PORT=SMTP server port
|
||||||
|
# - EMAIL_HOST_USER=
|
||||||
|
# - EMAIL_HOST_PASSWORD=
|
||||||
|
# - EMAIL_USE_TLS=True
|
||||||
|
# - EMAIL_FROM=no-reply@example.com #默认发件邮箱地址
|
||||||
|
ports:
|
||||||
|
- '8000:8000'
|
||||||
|
networks:
|
||||||
|
- chatgpt_ui_network
|
||||||
|
backend-web-server:
|
||||||
|
image: wongsaang/chatgpt-ui-web-server:latest
|
||||||
|
environment:
|
||||||
|
- BACKEND_URL=http://backend-wsgi-server:8000
|
||||||
|
ports:
|
||||||
|
- '9000:80'
|
||||||
|
depends_on:
|
||||||
|
- backend-wsgi-server
|
||||||
|
networks:
|
||||||
|
- chatgpt_ui_network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
chatgpt_ui_network:
|
||||||
|
driver: bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 启动服务
|
||||||
|
|
||||||
|
你可以自行修改配置后,运行下面的命令来启动服务。
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose up --pull always -d
|
||||||
|
```
|
||||||
|
|
||||||
|
这个命令用于启动 Docker Compose 配置中的服务。具体的参数含义如下:
|
||||||
|
|
||||||
|
- `up`:启动 Docker Compose 配置中的服务。
|
||||||
|
- `--pull always`:每次启动服务前,都会从 Docker 镜像仓库中拉取最新版本的镜像。这样可以确保使用的镜像始终是最新的。
|
||||||
|
- `-d`:在后台运行服务。如果不加这个参数,服务会在当前终端窗口中运行,直到用户手动停止服务。
|
||||||
|
|
||||||
|
|
||||||
|
## 部署完成之后
|
||||||
|
|
||||||
|
访问 `http(s)://your.domain:9000/admin` 或 IP `http(s)://123.123.123.123:9000/admin` 登录管理面板。
|
||||||
|
|
||||||
|
默认超级用户: **admin**
|
||||||
|
|
||||||
|
默认密码: **password**
|
||||||
|
|
||||||
|
~~在可以开始聊天之前,您需要添加一个 OpenAI 的 API 密钥。在管理面板的设置模型中,有一个名称为 `openai_api_key` 的记录,将值设置为您的 API 密钥。~~
|
||||||
|
|
||||||
|
在最新版本中,管理面板增加了一个独立的 API Key 的管理,位于管理面板的 `Provider/ Api keys`。你可以在这里添加多个 API Key,后端程序会统计每个 Key 的 token 使用量,并根据 token 使用量来平衡使用 Key。**想要这个功能生效,需要删除之前的`openai_api_key`设置**
|
||||||
|
|
||||||
|
现在可以访问客户端地址 `http(s)://your.domain` 或 IP `http://123.123.123.123` 开始聊天。
|
||||||
|
|
||||||
|
🎉🎉🎉 祝开心!
|
||||||
@@ -1,4 +1,21 @@
|
|||||||
{
|
{
|
||||||
|
"signIn":"Sign In",
|
||||||
|
"signUp":"Sign Up",
|
||||||
|
"username":"User Name",
|
||||||
|
"password":"Password",
|
||||||
|
"Username is required":"Username is required",
|
||||||
|
"Password is required":"Password is required",
|
||||||
|
"Create your account":"Create your account",
|
||||||
|
"createAccount":"Create Account",
|
||||||
|
"email":"E-mail",
|
||||||
|
"Sign in instead":"Sign in instead",
|
||||||
|
"Please enter your username":"Please enter your username",
|
||||||
|
"Username must be at least 4 characters":"Username must be at least 4 characters",
|
||||||
|
"Please enter your e-mail address":"Please enter your e-mail address",
|
||||||
|
"E-mail address must be valid":"E-mail address must be valid",
|
||||||
|
"Please enter your password":"Please enter your password",
|
||||||
|
"Password must be at least 8 characters":"Password must be at least 8 characters",
|
||||||
|
"Please confirm your password":"Please confirm your password",
|
||||||
"welcomeTo": "Welcome to",
|
"welcomeTo": "Welcome to",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"setApiKey": "Set API Key",
|
"setApiKey": "Set API Key",
|
||||||
@@ -10,6 +27,10 @@
|
|||||||
"saveAndClose": "Save & Close",
|
"saveAndClose": "Save & Close",
|
||||||
"pleaseSelectAtLeastOneModelDot": "Please select at least one model.",
|
"pleaseSelectAtLeastOneModelDot": "Please select at least one model.",
|
||||||
"writeAMessage": "Write a message",
|
"writeAMessage": "Write a message",
|
||||||
|
"frequentlyPrompts": "Frequently prompts",
|
||||||
|
"addPrompt": "Add prompt",
|
||||||
|
"titlePrompt": "Title",
|
||||||
|
"addNewPrompt": "Add a new prompt",
|
||||||
"pressEnterToSendYourMessageOrShiftEnterToAddANewLine": "Press Enter to send your message or Shift+Enter to add a new line",
|
"pressEnterToSendYourMessageOrShiftEnterToAddANewLine": "Press Enter to send your message or Shift+Enter to add a new line",
|
||||||
"lightMode": "Light Mode",
|
"lightMode": "Light Mode",
|
||||||
"darkMode": "Dark Mode",
|
"darkMode": "Dark Mode",
|
||||||
@@ -17,6 +38,7 @@
|
|||||||
"themeMode": "Theme Mode",
|
"themeMode": "Theme Mode",
|
||||||
"feedback": "Feedback",
|
"feedback": "Feedback",
|
||||||
"newConversation": "New conversation",
|
"newConversation": "New conversation",
|
||||||
|
"defaultConversationTitle": "Unnamed",
|
||||||
"clearConversations": "Clear conversations",
|
"clearConversations": "Clear conversations",
|
||||||
"modelParameters": "Model Parameters",
|
"modelParameters": "Model Parameters",
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
@@ -34,8 +56,21 @@
|
|||||||
"copied": "Copied",
|
"copied": "Copied",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"signOut": "Sign out",
|
"signOut": "Sign out",
|
||||||
|
"resetPassword": "Reset password",
|
||||||
|
"submit": "Submit",
|
||||||
|
"agree": "Agree",
|
||||||
|
"newPassword": "New password",
|
||||||
|
"currentPassword": "Current password",
|
||||||
|
"confirmPassword": "Confirm password",
|
||||||
|
"yourPasswordHasBeenReset": "Your password has been reset",
|
||||||
|
"nowYouNeedToSignInAgain": "Now you need to sign in again",
|
||||||
"webSearch": "Web Search",
|
"webSearch": "Web Search",
|
||||||
"webSearchDefaultPrompt": "Web search results:\n\n[web_results]\nCurrent date: [current_date]\n\nInstructions: Using the provided web search results, write a comprehensive reply to the given query. Make sure to cite results using [[number](URL)] notation after the reference. If the provided search results refer to multiple subjects with the same name, write separate answers for each subject.\nQuery: [query]",
|
"webSearchDefaultPrompt": "Web search results:\n\n[web_results]\nCurrent date: [current_date]\n\nInstructions: Using the provided web search results, write a comprehensive reply to the given query. Make sure to cite results using [[number](URL)] notation after the reference. If the provided search results refer to multiple subjects with the same name, write separate answers for each subject.\nQuery: [query]",
|
||||||
|
"genTitlePrompt": "Generate a short title for the following content, no more than 10 words. \n\nContent: ",
|
||||||
|
"maxTokenTips1": "The maximum context length of the current model is",
|
||||||
|
"maxTokenTips2": "token, which includes the length of the prompt and the length of the generated text. The `Max Tokens` here refers to the length of the generated text. Therefore, you should leave some space for your prompt and not set it too large or to the maximum.",
|
||||||
|
"frugalMode": "Frugal mode",
|
||||||
|
"frugalModeTip": "Activate frugal mode, the client will not send historical messages to ChatGPT, which can save token consumption. If you want ChatGPT to understand the context of the conversation, please turn off frugal mode.",
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"introduction1": "is an unofficial client for ChatGPT, but uses the official OpenAI API.",
|
"introduction1": "is an unofficial client for ChatGPT, but uses the official OpenAI API.",
|
||||||
"introduction2": "You will need an OpenAI API Key before you can use this client.",
|
"introduction2": "You will need an OpenAI API Key before you can use this client.",
|
||||||
|
|||||||
96
lang/fr-FR.json
Normal file
96
lang/fr-FR.json
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"signIn":"Se connecter",
|
||||||
|
"signUp":"S'inscrire",
|
||||||
|
"username":"Nom d'utilisateur",
|
||||||
|
"password":"Mot de passe",
|
||||||
|
"Username is required":"Nom d'utilisateur requis",
|
||||||
|
"Password is required":"Mot de passe requis",
|
||||||
|
"Create your account":"Créer votre compte",
|
||||||
|
"createAccount":"Créer un compte",
|
||||||
|
"email":"E-mail",
|
||||||
|
"Sign in instead":"S'identifier à la place",
|
||||||
|
"Please enter your username":"Veuillez saisir votre nom d'utilisateur",
|
||||||
|
"Username must be at least 4 characters":"Le nom d'utilisateur doit comporter au moins 4 caractères",
|
||||||
|
"Please enter your e-mail address":"Veuillez saisir votre adresse e-mail",
|
||||||
|
"E-mail address must be valid":"L'adresse e-mail doit être valide",
|
||||||
|
"Please enter your password":"Veuillez saisir votre mot de passe",
|
||||||
|
"Password must be at least 8 characters":"Le mot de passe doit comporter au moins 8 caractères",
|
||||||
|
"Please confirm your password":"Veuillez confirmer votre mot de passe",
|
||||||
|
"welcomeTo": "Bienvenue à",
|
||||||
|
"language": "Langue",
|
||||||
|
"setApiKey": "Définir la clé API",
|
||||||
|
"setOpenAIApiKey": "Définir la clé API OpenAI",
|
||||||
|
"openAIApiKey": "Clé API OpenAI",
|
||||||
|
"getAKey": "Obtenir une clé",
|
||||||
|
"openAIModels": "Modèles OpenAI",
|
||||||
|
"aboutTheModels": "À propos des modèles",
|
||||||
|
"saveAndClose": "Enregistrer et fermer",
|
||||||
|
"pleaseSelectAtLeastOneModelDot": "Veuillez sélectionner au moins un modèle.",
|
||||||
|
"writeAMessage": "Écrire un message",
|
||||||
|
"frequentlyPrompts": "Prompts fréquents",
|
||||||
|
"addPrompt": "Ajouter un prompt",
|
||||||
|
"titlePrompt": "Titre",
|
||||||
|
"addNewPrompt": "Ajouter un nouveau prompt",
|
||||||
|
"pressEnterToSendYourMessageOrShiftEnterToAddANewLine": "Appuyez sur Entrée pour envoyer votre message ou sur Maj+Entrée pour ajouter une nouvelle ligne",
|
||||||
|
"lightMode": "Mode clair",
|
||||||
|
"darkMode": "Mode sombre",
|
||||||
|
"followSystem": "Suivre le système",
|
||||||
|
"themeMode": "Mode thème",
|
||||||
|
"feedback": "Commentaires",
|
||||||
|
"newConversation": "Nouvelle conversation",
|
||||||
|
"defaultConversationTitle": "Sans titre",
|
||||||
|
"clearConversations": "Effacer les conversations",
|
||||||
|
"modelParameters": "Paramètres du modèle",
|
||||||
|
"model": "Modèle",
|
||||||
|
"temperature": "Température",
|
||||||
|
"topP": "Top P",
|
||||||
|
"frequencyPenalty": "Pénalité de fréquence",
|
||||||
|
"presencePenalty": "Pénalité de présence",
|
||||||
|
"maxTokens": "Nombre maximal de jetons",
|
||||||
|
"roles": {
|
||||||
|
"me": "Moi",
|
||||||
|
"ai": "IA"
|
||||||
|
},
|
||||||
|
"edit": "Modifier",
|
||||||
|
"copy": "Copier",
|
||||||
|
"copied": "Copié",
|
||||||
|
"delete": "Supprimer",
|
||||||
|
"signOut": "Déconnexion",
|
||||||
|
"resetPassword": "Réinitialiser le mot de passe",
|
||||||
|
"submit": "Soumettre",
|
||||||
|
"agree": "Accepter",
|
||||||
|
"newPassword": "Nouveau mot de passe",
|
||||||
|
"currentPassword": "Mot de passe actuel",
|
||||||
|
"confirmPassword": "Confirmer le mot de passe",
|
||||||
|
"yourPasswordHasBeenReset": "Votre mot de passe a été réinitialisé",
|
||||||
|
"nowYouNeedToSignInAgain": "Vous devez maintenant vous reconnecter",
|
||||||
|
"webSearch": "Recherche Web",
|
||||||
|
"webSearchDefaultPrompt": "Résultats de la recherche Web : \n\n[résultats_web]\nDate actuelle : [date_actuelle]\n\nInstructions : Utilisez les résultats de la recherche Web fournis pour rédiger une réponse complète à la question donnée. Assurez-vous de citer les résultats en utilisant la notation [nombre] après la référence. Si les résultats de recherche fournis font référence à plusieurs sujets avec le même nom, rédigez des réponses distinctes pour chaque sujet. \nQuestion : [question]",
|
||||||
|
"genTitlePrompt": "Générer un titre court pour le contenu suivant, pas plus de 10 mots. \n\nContenu : ",
|
||||||
|
"maxTokenTips1": "La longueur maximale du contexte pour le modèle actuel est de",
|
||||||
|
"maxTokenTips2": "jeton, ce qui inclut la longueur du prompt et la longueur du texte généré. Le paramètre Max Tokens ici fait référence à la longueur du texte généré. Vous devriez donc laisser de l'espace pour votre prompt et ne pas le régler trop grand ou à la limite maximale.",
|
||||||
|
"frugalMode": "Mode éco",
|
||||||
|
"frugalModeTip": "Activez le mode frugal, le client n'enverra pas les messages historiques à ChatGPT, ce qui peut économiser la consommation de jetons. Si vous souhaitez que ChatGPT comprenne le contexte de la conversation, veuillez désactiver le mode frugal.",
|
||||||
|
"welcomeScreen": {
|
||||||
|
"introduction1": "est un client non officiel pour ChatGPT, mais utilise l'API officielle d'OpenAI.",
|
||||||
|
"introduction2": "Vous aurez besoin d'une clé API OpenAI avant de pouvoir utiliser ce client.",
|
||||||
|
"examples": {
|
||||||
|
"title": "Exemples",
|
||||||
|
"item1": "\"Expliquez l'informatique quantique en termes simples\"",
|
||||||
|
"item2": "\"Avez-vous des idées créatives pour l'anniversaire d'un enfant de 10 ans?\"",
|
||||||
|
"item3": "\"Comment faire une requête HTTP en JavaScript?\""
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"title": "Fonctionnalités",
|
||||||
|
"item1": "Se souvient de ce que l'utilisateur a dit précédemment dans la conversation",
|
||||||
|
"item2": "Permet à l'utilisateur de fournir des corrections de suivi",
|
||||||
|
"item3": "Entraîné à refuser les demandes inappropriées"
|
||||||
|
},
|
||||||
|
"limitations": {
|
||||||
|
"title": "Limitations",
|
||||||
|
"item1": "Peut occasionnellement générer des informations incorrectes",
|
||||||
|
"item2": "Peut occasionnellement produire des instructions dangereuses ou du contenu biaisé",
|
||||||
|
"item3": "Connaissance limitée du monde et des événements après 2021"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
96
lang/ru-RU.json
Normal file
96
lang/ru-RU.json
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"signIn":"Sign In",
|
||||||
|
"signUp":"Sign Up",
|
||||||
|
"username":"User Name",
|
||||||
|
"password":"Password",
|
||||||
|
"Username is required":"Username is required",
|
||||||
|
"Password is required":"Password is required",
|
||||||
|
"Create your account":"Create your account",
|
||||||
|
"createAccount":"Create Account",
|
||||||
|
"email":"E-mail",
|
||||||
|
"Sign in instead":"Sign in instead",
|
||||||
|
"Please enter your username":"Please enter your username",
|
||||||
|
"Username must be at least 4 characters":"Username must be at least 4 characters",
|
||||||
|
"Please enter your e-mail address":"Please enter your e-mail address",
|
||||||
|
"E-mail address must be valid":"E-mail address must be valid",
|
||||||
|
"Please enter your password":"Please enter your password",
|
||||||
|
"Password must be at least 8 characters":"Password must be at least 8 characters",
|
||||||
|
"Please confirm your password":"Please confirm your password",
|
||||||
|
"welcomeTo": "Добро пожаловать в",
|
||||||
|
"language": "Язык",
|
||||||
|
"setApiKey": "Установить ключ API",
|
||||||
|
"setOpenAIApiKey": "Установить ключ API OpenAI",
|
||||||
|
"openAIApiKey": "Ключ API OpenAI",
|
||||||
|
"getAKey": "Получить ключ",
|
||||||
|
"openAIModels": "Модели OpenAI",
|
||||||
|
"aboutTheModels": "О моделях",
|
||||||
|
"saveAndClose": "Сохранить & Закрыть",
|
||||||
|
"pleaseSelectAtLeastOneModelDot": "Выберите хотя бы одну модель.",
|
||||||
|
"writeAMessage": "Напишите сообщение",
|
||||||
|
"frequentlyPrompts": "Список подсказок",
|
||||||
|
"addPrompt": "Добавить подсказку",
|
||||||
|
"titlePrompt": "Заголовок",
|
||||||
|
"addNewPrompt": "Добавитьте новую подсказку",
|
||||||
|
"pressEnterToSendYourMessageOrShiftEnterToAddANewLine": "Нажмите Enter, чтобы отправить сообщение, или Shift+Enter, чтобы добавить новую строку.",
|
||||||
|
"lightMode": "Светлая",
|
||||||
|
"darkMode": "Темная",
|
||||||
|
"followSystem": "Системная",
|
||||||
|
"themeMode": "Тема",
|
||||||
|
"feedback": "Обратная связь",
|
||||||
|
"newConversation": "Новый чат",
|
||||||
|
"defaultConversationTitle": "Безымянный",
|
||||||
|
"clearConversations": "Очистить чаты",
|
||||||
|
"modelParameters": "Параметры модели",
|
||||||
|
"model": "Модель",
|
||||||
|
"temperature": "Temperature",
|
||||||
|
"topP": "Top P",
|
||||||
|
"frequencyPenalty": "Frequency Penalty",
|
||||||
|
"presencePenalty": "Presence Penalty",
|
||||||
|
"maxTokens": "Max Tokens",
|
||||||
|
"roles": {
|
||||||
|
"me": "Я",
|
||||||
|
"ai": "AI"
|
||||||
|
},
|
||||||
|
"edit": "Редактировать",
|
||||||
|
"copy": "Копировать",
|
||||||
|
"copied": "Скопировано",
|
||||||
|
"delete": "Удалить",
|
||||||
|
"signOut": "Выход",
|
||||||
|
"resetPassword": "Сбросить пароль",
|
||||||
|
"submit": "Отправить",
|
||||||
|
"agree": "Согласен",
|
||||||
|
"newPassword": "Новый пароль",
|
||||||
|
"currentPassword": "Текущий пароль",
|
||||||
|
"confirmPassword": "Подтвердите пароль",
|
||||||
|
"yourPasswordHasBeenReset": "Ваш пароль был сброшен",
|
||||||
|
"nowYouNeedToSignInAgain": "Теперь вам нужно снова войти в систему",
|
||||||
|
"webSearch": "Поиск в интернете",
|
||||||
|
"webSearchDefaultPrompt": "Результаты веб-поиска:\n\n[web_results]\nТекущая дата: [current_date]\n\nИнструкции: Используя предоставленные результаты веб-поиска, напишите развернутый ответ на заданный запрос. Обязательно цитируйте результаты, используя обозначение [[number](URL)] после ссылки. Если предоставленные результаты поиска относятся к нескольким темам с одинаковым названием, напишите отдельные ответы для каждой темы.\nЗапрос: [query]",
|
||||||
|
"genTitlePrompt": "Придумайте короткий заголовок для следующего содержания, не более 10 слов. \n\nСодержание: ",
|
||||||
|
"maxTokenTips1": "The maximum context length of the current model is",
|
||||||
|
"maxTokenTips2": "token, which includes the length of the prompt and the length of the generated text. The `Max Tokens` here refers to the length of the generated text. Therefore, you should leave some space for your prompt and not set it too large or to the maximum.",
|
||||||
|
"frugalMode": "Frugal mode",
|
||||||
|
"frugalModeTip": "Activate frugal mode, the client will not send historical messages to ChatGPT, which can save token consumption. If you want ChatGPT to understand the context of the conversation, please turn off frugal mode.",
|
||||||
|
"welcomeScreen": {
|
||||||
|
"introduction1": "является неофициальным клиентом для ChatGPT, но использует официальный API OpenAI.",
|
||||||
|
"introduction2": "Вам понадобится ключ API OpenAI, прежде чем вы сможете использовать этот клиент.",
|
||||||
|
"examples": {
|
||||||
|
"title": "Примеры",
|
||||||
|
"item1": "\"Объясни, что такое квантовые вычисления простыми словами\"",
|
||||||
|
"item2": "\"Предложи несколько креативных идей для дня рождения 10-летнего ребенка?\"",
|
||||||
|
"item3": "\"Как сделать HTTP-запрос в Javascript?\""
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"title": "Возможности",
|
||||||
|
"item1": "Помнит, что пользователь сказал ранее в разговоре",
|
||||||
|
"item2": "Позволяет пользователю вносить последующие исправления",
|
||||||
|
"item3": "Научен отклонять неуместные запросы"
|
||||||
|
},
|
||||||
|
"limitations": {
|
||||||
|
"title": "Ограничения",
|
||||||
|
"item1": "Иногда может генерировать неверную информацию",
|
||||||
|
"item2": "Иногда может создавать вредные инструкции или предвзятый контент",
|
||||||
|
"item3": "Ограниченное знание мира и событий после 2021 года"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,32 @@
|
|||||||
{
|
{
|
||||||
|
"invitation code":"邀请码",
|
||||||
|
"Please enter your code":"请填写邀请码",
|
||||||
|
"signIn":"登录",
|
||||||
|
"signUp":"注册",
|
||||||
|
"username":"用户名",
|
||||||
|
"password":"密码",
|
||||||
|
"Username is required":"请填写用户名",
|
||||||
|
"Password is required":"请填写密码",
|
||||||
|
"Create your account":"创建你的账号",
|
||||||
|
"createAccount":"创建账号",
|
||||||
|
"email":"邮箱",
|
||||||
|
"Sign in instead":"返回登录",
|
||||||
|
"Please enter your username":"请输入你的用户名",
|
||||||
|
"Username must be at least 4 characters":"用户名至少四个字符",
|
||||||
|
"Please enter your e-mail address":"请输入你的电子邮箱",
|
||||||
|
"E-mail address must be valid":"电子邮箱地址格式不正确",
|
||||||
|
"Please enter your password":"请输入你的密码",
|
||||||
|
"Password must be at least 8 characters":"密码至少八个字符",
|
||||||
|
"Please confirm your password":"请输入确认密码",
|
||||||
|
"Something went wrong. Please try again.":"网络错误请稍后重试",
|
||||||
|
"This password is too common.":"密码过于简单",
|
||||||
|
"This password is entirely numeric.":"密码不能全是数字",
|
||||||
|
"Your registration is successful":"恭喜你,注册成功!",
|
||||||
|
"You can now":"现在你可以",
|
||||||
|
"to your account.":"你的账号了。",
|
||||||
"welcomeTo": "欢迎来到",
|
"welcomeTo": "欢迎来到",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
"setApiKey": "设置API密钥",
|
"setApiKey": "API 密钥",
|
||||||
"setOpenAIApiKey": "设置OpenAI的API密钥",
|
"setOpenAIApiKey": "设置OpenAI的API密钥",
|
||||||
"openAIApiKey": "OpenAI的API密钥",
|
"openAIApiKey": "OpenAI的API密钥",
|
||||||
"getAKey": "获取钥匙",
|
"getAKey": "获取钥匙",
|
||||||
@@ -10,6 +35,10 @@
|
|||||||
"saveAndClose": "保存并关闭",
|
"saveAndClose": "保存并关闭",
|
||||||
"pleaseSelectAtLeastOneModelDot": "请至少选择一个模型",
|
"pleaseSelectAtLeastOneModelDot": "请至少选择一个模型",
|
||||||
"writeAMessage": "输入信息",
|
"writeAMessage": "输入信息",
|
||||||
|
"frequentlyPrompts": "Frequently prompts",
|
||||||
|
"addPrompt": "Add prompt",
|
||||||
|
"titlePrompt": "Title",
|
||||||
|
"addNewPrompt": "Add a new prompt",
|
||||||
"pressEnterToSendYourMessageOrShiftEnterToAddANewLine": "按回车键发送您的信息,或按Shift+Enter键添加新行",
|
"pressEnterToSendYourMessageOrShiftEnterToAddANewLine": "按回车键发送您的信息,或按Shift+Enter键添加新行",
|
||||||
"lightMode": "明亮模式",
|
"lightMode": "明亮模式",
|
||||||
"darkMode": "暗色模式",
|
"darkMode": "暗色模式",
|
||||||
@@ -17,6 +46,7 @@
|
|||||||
"themeMode": "主题模式",
|
"themeMode": "主题模式",
|
||||||
"feedback": "反馈",
|
"feedback": "反馈",
|
||||||
"newConversation": "新的对话",
|
"newConversation": "新的对话",
|
||||||
|
"defaultConversationTitle": "未命名",
|
||||||
"clearConversations": "清除对话",
|
"clearConversations": "清除对话",
|
||||||
"modelParameters": "模型参数",
|
"modelParameters": "模型参数",
|
||||||
"model": "模型",
|
"model": "模型",
|
||||||
@@ -34,8 +64,21 @@
|
|||||||
"copied": "已复制",
|
"copied": "已复制",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"signOut": "退出登录",
|
"signOut": "退出登录",
|
||||||
|
"resetPassword": "重置密码",
|
||||||
|
"submit": "提交",
|
||||||
|
"agree": "同意",
|
||||||
|
"newPassword": "新密码",
|
||||||
|
"currentPassword": "当前密码",
|
||||||
|
"confirmPassword": "确认密码",
|
||||||
|
"yourPasswordHasBeenReset": "您的密码已重置",
|
||||||
|
"nowYouNeedToSignInAgain": "现在您需要再次登录",
|
||||||
"webSearch": "网页搜索",
|
"webSearch": "网页搜索",
|
||||||
"webSearchDefaultPrompt": "网络搜索结果:\n\n[web_results]\n当前日期:[current_date]\n\n说明:使用提供的网络搜索结果,对给定的查询写出全面的回复。确保在引用参考文献后使用 [[number](URL)] 符号进行引用结果. 如果提供的搜索结果涉及到多个具有相同名称的主题,请针对每个主题编写单独的答案。\n查询:[query]",
|
"webSearchDefaultPrompt": "网络搜索结果:\n\n[web_results]\n当前日期:[current_date]\n\n说明:使用提供的网络搜索结果,对给定的查询写出全面的回复。确保在引用参考文献后使用 [[number](URL)] 符号进行引用结果. 如果提供的搜索结果涉及到多个具有相同名称的主题,请针对每个主题编写单独的答案。\n查询:[query]",
|
||||||
|
"genTitlePrompt": "为以下内容生成一个不超过10个字的简短标题。 \n\n内容: ",
|
||||||
|
"maxTokenTips1": "当前模型的最大上下文长度为",
|
||||||
|
"maxTokenTips2": "个 token,它包括了指令的长度和生成的文本长度。此处的最大 token 数量是指生成的文本长度。所以您应该为您的指令预留一些空间,不宜设置过大或拉满。",
|
||||||
|
"frugalMode": "节俭模式",
|
||||||
|
"frugalModeTip": "开启节俭模式,客户端不会把历史消息发送给ChatGPT,可以节省 token 的消耗。如果你想让 ChatGPT 了解对话的上下文,请关闭节俭模式。",
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"introduction1": "是一个非官方的ChatGPT客户端,但使用OpenAI的官方API",
|
"introduction1": "是一个非官方的ChatGPT客户端,但使用OpenAI的官方API",
|
||||||
"introduction2": "在使用本客户端之前,您需要一个OpenAI API密钥。",
|
"introduction2": "在使用本客户端之前,您需要一个OpenAI API密钥。",
|
||||||
|
|||||||
@@ -1,324 +1,8 @@
|
|||||||
<script setup>
|
|
||||||
import {useDisplay} from "vuetify";
|
|
||||||
|
|
||||||
const { $i18n, $auth } = useNuxtApp()
|
|
||||||
const runtimeConfig = useRuntimeConfig()
|
|
||||||
const colorMode = useColorMode()
|
|
||||||
const drawer = ref(null)
|
|
||||||
const themes = ref([
|
|
||||||
{ title: $i18n.t('lightMode'), value: 'light' },
|
|
||||||
{ title: $i18n.t('darkMode'), value: 'dark' },
|
|
||||||
{ title: $i18n.t('followSystem'), value: 'system'}
|
|
||||||
])
|
|
||||||
const setTheme = (theme) => {
|
|
||||||
colorMode.preference = theme
|
|
||||||
}
|
|
||||||
const feedback = () => {
|
|
||||||
window.open('https://github.com/WongSaang/chatgpt-ui/issues', '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
const { locale, locales, setLocale } = useI18n()
|
|
||||||
const setLang = (lang) => {
|
|
||||||
setLocale(lang)
|
|
||||||
}
|
|
||||||
|
|
||||||
const conversations = useConversions()
|
|
||||||
const currentConversation = useConversion()
|
|
||||||
|
|
||||||
const editingConversation = ref(null)
|
|
||||||
const deletingConversationIndex = ref(null)
|
|
||||||
|
|
||||||
const editConversation = (index) => {
|
|
||||||
editingConversation.value = conversations.value[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateConversation = async (index) => {
|
|
||||||
editingConversation.value.updating = true
|
|
||||||
const { data, error } = await useAuthFetch(`/api/chat/conversations/${editingConversation.value.id}/`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify({
|
|
||||||
topic: editingConversation.value.topic
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if (!error.value) {
|
|
||||||
conversations.value[index] = editingConversation.value
|
|
||||||
}
|
|
||||||
editingConversation.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteConversation = async (index) => {
|
|
||||||
deletingConversationIndex.value = index
|
|
||||||
const { data, error } = await useAuthFetch(`/api/chat/conversations/${conversations.value[index].id}/`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
})
|
|
||||||
deletingConversationIndex.value = null
|
|
||||||
if (!error.value) {
|
|
||||||
if (conversations.value[index].id === currentConversation.value.id) {
|
|
||||||
createNewConversion()
|
|
||||||
}
|
|
||||||
conversations.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearConversations = async () => {
|
|
||||||
deletingConversations.value = true
|
|
||||||
const { data, error } = await useAuthFetch(`/api/chat/conversations/delete_all`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
})
|
|
||||||
if (!error.value) {
|
|
||||||
loadConversations()
|
|
||||||
clearConfirmDialog.value = false
|
|
||||||
}
|
|
||||||
deletingConversations.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearConfirmDialog = ref(false)
|
|
||||||
const deletingConversations = ref(false)
|
|
||||||
const loadingConversations = ref(false)
|
|
||||||
|
|
||||||
const loadConversations = async () => {
|
|
||||||
loadingConversations.value = true
|
|
||||||
conversations.value = await getConversions()
|
|
||||||
loadingConversations.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const {mdAndUp} = useDisplay()
|
|
||||||
|
|
||||||
|
|
||||||
const drawerPermanent = computed(() => {
|
|
||||||
return mdAndUp.value
|
|
||||||
})
|
|
||||||
|
|
||||||
const signOut = async () => {
|
|
||||||
const { data, error } = await useFetch('/api/account/logout/', {
|
|
||||||
method: 'POST'
|
|
||||||
})
|
|
||||||
if (!error.value) {
|
|
||||||
await $auth.logout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
loadConversations()
|
|
||||||
loadSettings()
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-app
|
<v-app
|
||||||
:theme="$colorMode.value"
|
:theme="$colorMode.value"
|
||||||
>
|
>
|
||||||
<v-navigation-drawer
|
<NavigationDrawer />
|
||||||
v-model="drawer"
|
<slot />
|
||||||
:permanent="drawerPermanent"
|
|
||||||
width="300"
|
|
||||||
>
|
|
||||||
<div class="px-2 py-2">
|
|
||||||
<v-list>
|
|
||||||
<v-list-item>
|
|
||||||
<v-btn
|
|
||||||
block
|
|
||||||
variant="outlined"
|
|
||||||
prepend-icon="add"
|
|
||||||
@click="createNewConversion()"
|
|
||||||
class="text-none"
|
|
||||||
>
|
|
||||||
{{ $t('newConversation') }}
|
|
||||||
</v-btn>
|
|
||||||
</v-list-item>
|
|
||||||
<v-list-item v-show="loadingConversations">
|
|
||||||
<v-list-item-title class="d-flex justify-center">
|
|
||||||
<v-progress-circular indeterminate></v-progress-circular>
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
|
|
||||||
<v-list>
|
|
||||||
<template
|
|
||||||
v-for="(conversation, cIdx) in conversations"
|
|
||||||
:key="conversation.id"
|
|
||||||
>
|
|
||||||
<v-list-item
|
|
||||||
active-color="primary"
|
|
||||||
rounded="xl"
|
|
||||||
v-if="editingConversation && editingConversation.id === conversation.id"
|
|
||||||
>
|
|
||||||
<v-text-field
|
|
||||||
v-model="editingConversation.topic"
|
|
||||||
:loading="editingConversation.updating"
|
|
||||||
variant="underlined"
|
|
||||||
append-icon="done"
|
|
||||||
hide-details
|
|
||||||
density="compact"
|
|
||||||
autofocus
|
|
||||||
@keyup.enter="updateConversation(cIdx)"
|
|
||||||
@click:append="updateConversation(cIdx)"
|
|
||||||
></v-text-field>
|
|
||||||
</v-list-item>
|
|
||||||
<v-hover
|
|
||||||
v-if="!editingConversation || editingConversation.id !== conversation.id"
|
|
||||||
v-slot="{ isHovering, props }"
|
|
||||||
>
|
|
||||||
<v-list-item
|
|
||||||
rounded="xl"
|
|
||||||
active-color="primary"
|
|
||||||
@click="openConversationMessages(conversation)"
|
|
||||||
v-bind="props"
|
|
||||||
>
|
|
||||||
<v-list-item-title>{{ conversation.topic }}</v-list-item-title>
|
|
||||||
<template v-slot:append>
|
|
||||||
<div
|
|
||||||
v-show="isHovering"
|
|
||||||
>
|
|
||||||
<v-btn
|
|
||||||
icon="edit"
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
@click.stop="editConversation(cIdx)"
|
|
||||||
>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon="delete"
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
:loading="deletingConversationIndex === cIdx"
|
|
||||||
@click.stop="deleteConversation(cIdx)"
|
|
||||||
>
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</v-list-item>
|
|
||||||
</v-hover>
|
|
||||||
</template>
|
|
||||||
</v-list>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-slot:append>
|
|
||||||
<div class="px-1">
|
|
||||||
<v-divider></v-divider>
|
|
||||||
<v-list>
|
|
||||||
|
|
||||||
<v-dialog
|
|
||||||
v-model="clearConfirmDialog"
|
|
||||||
persistent
|
|
||||||
>
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<v-list-item
|
|
||||||
v-bind="props"
|
|
||||||
rounded="xl"
|
|
||||||
prepend-icon="delete_forever"
|
|
||||||
:title="$t('clearConversations')"
|
|
||||||
></v-list-item>
|
|
||||||
</template>
|
|
||||||
<v-card>
|
|
||||||
<v-card-title class="text-h5">
|
|
||||||
Are you sure you want to delete all conversations?
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text>This will be a permanent deletion and cannot be retrieved once deleted. Please proceed with caution.</v-card-text>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-btn
|
|
||||||
color="green-darken-1"
|
|
||||||
variant="text"
|
|
||||||
@click="clearConfirmDialog = false"
|
|
||||||
class="text-none"
|
|
||||||
>
|
|
||||||
Cancel deletion
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
color="green-darken-1"
|
|
||||||
variant="text"
|
|
||||||
@click="clearConversations"
|
|
||||||
class="text-none"
|
|
||||||
:loading="deletingConversations"
|
|
||||||
>
|
|
||||||
Confirm deletion
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
|
|
||||||
<ModelParameters/>
|
|
||||||
|
|
||||||
<v-menu
|
|
||||||
>
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<v-list-item
|
|
||||||
v-bind="props"
|
|
||||||
rounded="xl"
|
|
||||||
:prepend-icon="$colorMode.value === 'light' ? 'light_mode' : 'dark_mode'"
|
|
||||||
:title="$t('themeMode')"
|
|
||||||
></v-list-item>
|
|
||||||
</template>
|
|
||||||
<v-list
|
|
||||||
bg-color="white"
|
|
||||||
>
|
|
||||||
<v-list-item
|
|
||||||
v-for="(theme, idx) in themes"
|
|
||||||
:key="idx"
|
|
||||||
@click="setTheme(theme.value)"
|
|
||||||
>
|
|
||||||
<v-list-item-title>{{ theme.title }}</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
|
|
||||||
<SettingsLanguages/>
|
|
||||||
|
|
||||||
<v-list-item
|
|
||||||
rounded="xl"
|
|
||||||
prepend-icon="help_outline"
|
|
||||||
:title="$t('feedback')"
|
|
||||||
@click="feedback"
|
|
||||||
></v-list-item>
|
|
||||||
|
|
||||||
<v-list-item
|
|
||||||
rounded="xl"
|
|
||||||
prepend-icon="logout"
|
|
||||||
:title="$t('signOut')"
|
|
||||||
@click="signOut"
|
|
||||||
></v-list-item>
|
|
||||||
|
|
||||||
</v-list>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</v-navigation-drawer>
|
|
||||||
|
|
||||||
<v-app-bar
|
|
||||||
class="d-md-none"
|
|
||||||
>
|
|
||||||
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
|
|
||||||
|
|
||||||
<v-toolbar-title>{{ runtimeConfig.public.appName }}</v-toolbar-title>
|
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
|
|
||||||
<v-btn
|
|
||||||
:title="$t('newConversation')"
|
|
||||||
icon="add"
|
|
||||||
@click="createNewConversion()"
|
|
||||||
></v-btn>
|
|
||||||
|
|
||||||
</v-app-bar>
|
|
||||||
|
|
||||||
<v-main>
|
|
||||||
<NuxtPage/>
|
|
||||||
</v-main>
|
|
||||||
|
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
|
||||||
.v-navigation-drawer__content::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
.v-navigation-drawer__content:hover::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
.v-navigation-drawer__content:hover::-webkit-scrollbar-thumb {
|
|
||||||
background-color: #999;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
18
middleware/auth.ts
Normal file
18
middleware/auth.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||||
|
const user = useUser()
|
||||||
|
const signInPath = '/account/signin'
|
||||||
|
if (!user.value && to.path !== signInPath) {
|
||||||
|
const { error, data} = await fetchUser()
|
||||||
|
if (error.value) {
|
||||||
|
return navigateTo({
|
||||||
|
path: signInPath,
|
||||||
|
query: {
|
||||||
|
callback: encodeURIComponent(to.fullPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setUser(data.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -2,9 +2,9 @@ server {
|
|||||||
listen 80;
|
listen 80;
|
||||||
listen [::]:80;
|
listen [::]:80;
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
|
root /app;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
root /app;
|
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
const appName = 'ChatGPT UI'
|
const appName = process.env.NUXT_PUBLIC_APP_NAME ?? 'ChatGPT UI'
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
dev: false,
|
debug: process.env.NODE_ENV !== 'production',
|
||||||
ssr: false,
|
ssr: process.env.SSR !== 'false',
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
title: appName,
|
title: appName,
|
||||||
@@ -14,6 +13,7 @@ export default defineNuxtConfig({
|
|||||||
appName: appName,
|
appName: appName,
|
||||||
typewriter: false,
|
typewriter: false,
|
||||||
typewriterDelay: 50,
|
typewriterDelay: 50,
|
||||||
|
customApiKey: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
@@ -27,7 +27,7 @@ export default defineNuxtConfig({
|
|||||||
modules: [
|
modules: [
|
||||||
'@kevinmarrec/nuxt-pwa',
|
'@kevinmarrec/nuxt-pwa',
|
||||||
'@nuxtjs/color-mode',
|
'@nuxtjs/color-mode',
|
||||||
'@nuxtjs/i18n',
|
'@nuxtjs/i18n'
|
||||||
],
|
],
|
||||||
pwa: {
|
pwa: {
|
||||||
manifest: {
|
manifest: {
|
||||||
@@ -53,23 +53,25 @@ export default defineNuxtConfig({
|
|||||||
iso: 'zh-CN',
|
iso: 'zh-CN',
|
||||||
name: '简体中文',
|
name: '简体中文',
|
||||||
file: 'zh-CN.json',
|
file: 'zh-CN.json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ru',
|
||||||
|
iso: 'ru-RU',
|
||||||
|
name: 'Русский',
|
||||||
|
file: 'ru-RU.json',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'fr',
|
||||||
|
iso: 'fr-FR',
|
||||||
|
name: 'Français',
|
||||||
|
file: 'fr-FR.json',
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
lazy: true,
|
lazy: true,
|
||||||
langDir: 'lang',
|
langDir: 'lang',
|
||||||
defaultLocale: 'en',
|
defaultLocale: process.env.DEFAULT_LOCALE || 'en',
|
||||||
vueI18n: {
|
vueI18n: {
|
||||||
fallbackLocale: 'en',
|
fallbackLocale: 'en',
|
||||||
},
|
},
|
||||||
},
|
|
||||||
nitro: {
|
|
||||||
devProxy: {
|
|
||||||
"/api": {
|
|
||||||
target: process.env.NUXT_DEV_SERVER ?? 'http://localhost:8000/api',
|
|
||||||
prependPath: true,
|
|
||||||
changeOrigin: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -5,22 +5,27 @@
|
|||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare",
|
||||||
|
"docs:dev": "vuepress dev docs",
|
||||||
|
"docs:build": "vuepress build docs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kevinmarrec/nuxt-pwa": "^0.17.0",
|
"@kevinmarrec/nuxt-pwa": "^0.17.0",
|
||||||
|
"@nuxt/devtools": "^0.4.0",
|
||||||
"@nuxtjs/color-mode": "^3.2.0",
|
"@nuxtjs/color-mode": "^3.2.0",
|
||||||
"@nuxtjs/i18n": "^8.0.0-beta.9",
|
"@nuxtjs/i18n": "^8.0.0-beta.9",
|
||||||
"@vite-pwa/nuxt": "^0.0.7",
|
|
||||||
"material-design-icons-iconfont": "^6.7.0",
|
"material-design-icons-iconfont": "^6.7.0",
|
||||||
"nuxt": "^3.2.0"
|
"nuxt": "^3.4.0",
|
||||||
|
"vuepress": "^2.0.0-beta.61"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@microsoft/fetch-event-source": "^2.0.1",
|
"@microsoft/fetch-event-source": "^2.0.1",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"highlight.js": "^11.7.0",
|
"highlight.js": "^11.7.0",
|
||||||
|
"http-proxy-middleware": "3.0.0-beta.1",
|
||||||
"is-mobile": "^3.1.1",
|
"is-mobile": "^3.1.1",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
|
"markdown-it-mathjax3": "^4.3.2",
|
||||||
"nanoid": "^4.0.1",
|
"nanoid": "^4.0.1",
|
||||||
"vuetify": "^3.0.6"
|
"vuetify": "^3.0.6"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const route = useRoute()
|
|||||||
const sending = ref(false)
|
const sending = ref(false)
|
||||||
const resent = ref(false)
|
const resent = ref(false)
|
||||||
const errorMsg = ref(null)
|
const errorMsg = ref(null)
|
||||||
|
const user = useUser()
|
||||||
const resendEmail = async () => {
|
const resendEmail = async () => {
|
||||||
errorMsg.value = null
|
errorMsg.value = null
|
||||||
sending.value = true
|
sending.value = true
|
||||||
@@ -46,15 +47,15 @@ onNuxtReady(() => {
|
|||||||
>
|
>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div v-if="route.query.email_verification_required && route.query.email_verification_required === 'none'">
|
<div v-if="route.query.email_verification_required && route.query.email_verification_required === 'none'">
|
||||||
<h2 class="text-h4">Your registration is successful</h2>
|
<h2 class="text-h4">{{$('Your registration is successful')}}</h2>
|
||||||
<p class="mt-5">
|
<p class="mt-5">
|
||||||
You can now <NuxtLink to="/account/signin">login</NuxtLink> to your account.
|
{{$('You can now')}} <NuxtLink to="/account/signin">{{$('signIn')}}</NuxtLink> {{$t('to your account.')}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<h2 class="text-h4">Verify your email</h2>
|
<h2 class="text-h4">Verify your email</h2>
|
||||||
<p class="mt-5">
|
<p class="mt-5">
|
||||||
We've sent a verification email to <strong>{{ $auth.user.email }}</strong>. <br>
|
We've sent a verification email to <strong>{{ user.email }}</strong>. <br>
|
||||||
Please check your inbox and click the link to verify your email address.
|
Please check your inbox and click the link to verify your email address.
|
||||||
</p>
|
</p>
|
||||||
<p v-if="errorMsg"
|
<p v-if="errorMsg"
|
||||||
|
|||||||
175
pages/account/resetPassword.vue
Normal file
175
pages/account/resetPassword.vue
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<script setup>
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ["auth"]
|
||||||
|
})
|
||||||
|
const formData = ref({
|
||||||
|
old_password: '',
|
||||||
|
new_password1: '',
|
||||||
|
new_password2: ''
|
||||||
|
})
|
||||||
|
const formRules = ref({
|
||||||
|
old_password: [
|
||||||
|
v => !!v || 'Current password is required'
|
||||||
|
],
|
||||||
|
new_password1: [
|
||||||
|
v => !!v || 'New password is required'
|
||||||
|
],
|
||||||
|
new_password2: [
|
||||||
|
v => !!v || 'Confirm password is required',
|
||||||
|
v => v === formData.value.new_password1 || 'Passwords do not match'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
const fieldErrors = ref({
|
||||||
|
old_password: '',
|
||||||
|
new_password1: '',
|
||||||
|
new_password2: '',
|
||||||
|
})
|
||||||
|
const errorMsg = ref(null)
|
||||||
|
const resetForm = ref(null)
|
||||||
|
const valid = ref(true)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const route = useRoute()
|
||||||
|
const passwordInputType = ref('password')
|
||||||
|
|
||||||
|
const signOut = async () => {
|
||||||
|
const { data, error } = await useFetch('/api/account/logout/', {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
if (!error.value) {
|
||||||
|
await logout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
errorMsg.value = null
|
||||||
|
const { valid } = await resetForm.value.validate()
|
||||||
|
if (valid) {
|
||||||
|
submitting.value = true
|
||||||
|
const { data, error } = await useFetch('/api/account/password/change/', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(formData.value)
|
||||||
|
})
|
||||||
|
submitting.value = false
|
||||||
|
if (error.value) {
|
||||||
|
if (error.value.status === 400) {
|
||||||
|
for (const key in formData.value) {
|
||||||
|
if (error.value.data[key]) {
|
||||||
|
fieldErrors.value[key] = error.value.data[key][0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (error.value.data.non_field_errors) {
|
||||||
|
errorMsg.value = error.value.data.non_field_errors[0]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (error.value.data.detail) {
|
||||||
|
errorMsg.value = error.value.data.detail
|
||||||
|
} else {
|
||||||
|
errorMsg.value = 'Something went wrong. Please try again.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
successDialog.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFieldUpdate = (field) => {
|
||||||
|
fieldErrors.value[field] = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const successDialog = ref(false)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-card
|
||||||
|
style="height: 100vh"
|
||||||
|
>
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
sm="9"
|
||||||
|
offset-sm="1"
|
||||||
|
md="6"
|
||||||
|
offset-md="3"
|
||||||
|
>
|
||||||
|
<v-card
|
||||||
|
class="mt-15"
|
||||||
|
elevation="0"
|
||||||
|
>
|
||||||
|
<div class="text-center text-h4">{{ $t('resetPassword') }}</div>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form ref="resetForm">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.old_password"
|
||||||
|
:rules="formRules.old_password"
|
||||||
|
:error-messages="fieldErrors.old_password"
|
||||||
|
@update:modelValue="handleFieldUpdate('old_password')"
|
||||||
|
:label="$t('currentPassword')"
|
||||||
|
variant="underlined"
|
||||||
|
clearable
|
||||||
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.new_password1"
|
||||||
|
:rules="formRules.new_password1"
|
||||||
|
:error-messages="fieldErrors.new_password1"
|
||||||
|
@update:modelValue="handleFieldUpdate('new_password1')"
|
||||||
|
:label="$t('newPassword')"
|
||||||
|
variant="underlined"
|
||||||
|
clearable
|
||||||
|
></v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.new_password2"
|
||||||
|
:rules="formRules.new_password2"
|
||||||
|
:error-messages="fieldErrors.new_password2"
|
||||||
|
@update:modelValue="handleFieldUpdate('new_password2')"
|
||||||
|
:label="$t('confirmPassword')"
|
||||||
|
variant="underlined"
|
||||||
|
clearable
|
||||||
|
></v-text-field>
|
||||||
|
</v-form>
|
||||||
|
|
||||||
|
<div v-if="errorMsg" class="text-red">{{ errorMsg }}</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="mt-5 d-flex justify-space-between"
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
block
|
||||||
|
color="primary"
|
||||||
|
:loading="submitting"
|
||||||
|
@click="submit"
|
||||||
|
size="large"
|
||||||
|
>{{ $t('submit') }}</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-dialog
|
||||||
|
v-model="successDialog"
|
||||||
|
persistent
|
||||||
|
width="auto"
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">
|
||||||
|
{{ $t('yourPasswordHasBeenReset') }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>{{ $t('nowYouNeedToSignInAgain') }}</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
color="green-darken-1"
|
||||||
|
variant="text"
|
||||||
|
@click="signOut"
|
||||||
|
>
|
||||||
|
{{ $t('agree') }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
style="height: 100vh"
|
style="height: 100vh"
|
||||||
>
|
>
|
||||||
<v-container>
|
<v-container>
|
||||||
|
<SettingsLanguages/>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col
|
<v-col
|
||||||
sm="9"
|
sm="9"
|
||||||
@@ -14,20 +15,20 @@
|
|||||||
class="mt-15"
|
class="mt-15"
|
||||||
elevation="0"
|
elevation="0"
|
||||||
>
|
>
|
||||||
<div class="text-center text-h4">Sign in</div>
|
<div class="text-center text-h4">{{$t('signIn')}}</div>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-form ref="signInForm">
|
<v-form ref="signInForm">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="formData.username"
|
v-model="formData.username"
|
||||||
:rules="formRules.username"
|
:rules="formRules.username"
|
||||||
label="User name"
|
:label="$t('username')"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
clearable
|
clearable
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="formData.password"
|
v-model="formData.password"
|
||||||
:rules="formRules.password"
|
:rules="formRules.password"
|
||||||
label="Password"
|
:label="$t('password')"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
clearable
|
clearable
|
||||||
@@ -35,7 +36,6 @@
|
|||||||
:append-inner-icon="passwordInputType === 'password' ? 'visibility' : 'visibility_off'"
|
:append-inner-icon="passwordInputType === 'password' ? 'visibility' : 'visibility_off'"
|
||||||
@click:append-inner="passwordInputType = passwordInputType === 'password' ? 'text' : 'password'"
|
@click:append-inner="passwordInputType = passwordInputType === 'password' ? 'text' : 'password'"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
</v-form>
|
</v-form>
|
||||||
|
|
||||||
<div v-if="errorMsg" class="text-red">{{ errorMsg }}</div>
|
<div v-if="errorMsg" class="text-red">{{ errorMsg }}</div>
|
||||||
@@ -47,14 +47,14 @@
|
|||||||
@click="navigateTo('/account/signup')"
|
@click="navigateTo('/account/signup')"
|
||||||
variant="text"
|
variant="text"
|
||||||
color="primary"
|
color="primary"
|
||||||
>Create account</v-btn>
|
>{{$t('createAccount')}}</v-btn>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
:loading="submitting"
|
:loading="submitting"
|
||||||
@click="submit"
|
@click="submit"
|
||||||
size="large"
|
size="large"
|
||||||
>Submit</v-btn>
|
>{{$t('signIn')}}</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
@@ -67,6 +67,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import {useUser} from "~/composables/states";
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'vuetify-app'
|
layout: 'vuetify-app'
|
||||||
})
|
})
|
||||||
@@ -76,16 +78,14 @@ const formData = ref({
|
|||||||
})
|
})
|
||||||
const formRules = ref({
|
const formRules = ref({
|
||||||
username: [
|
username: [
|
||||||
v => !!v || 'Username is required'
|
v => !!v || $i18n.t('Username is required')
|
||||||
],
|
],
|
||||||
password: [
|
password: [
|
||||||
v => !!v || 'Password is required'
|
v => !!v || $i18n.t('Password is required')
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
const { $auth } = useNuxtApp()
|
|
||||||
const errorMsg = ref(null)
|
const errorMsg = ref(null)
|
||||||
const signInForm = ref(null)
|
const signInForm = ref(null)
|
||||||
const valid = ref(true)
|
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const passwordInputType = ref('password')
|
const passwordInputType = ref('password')
|
||||||
@@ -99,6 +99,7 @@ const submit = async () => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(formData.value)
|
body: JSON.stringify(formData.value)
|
||||||
})
|
})
|
||||||
|
submitting.value = false
|
||||||
if (error.value) {
|
if (error.value) {
|
||||||
if (error.value.status === 400) {
|
if (error.value.status === 400) {
|
||||||
if (error.value.data.non_field_errors) {
|
if (error.value.data.non_field_errors) {
|
||||||
@@ -108,10 +109,10 @@ const submit = async () => {
|
|||||||
errorMsg.value = 'Something went wrong. Please try again.'
|
errorMsg.value = 'Something went wrong. Please try again.'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$auth.setUser(data.value.user)
|
setUser(data.value.user)
|
||||||
navigateTo(route.query.callback || '/')
|
const callback = route.query.callback ? decodeURIComponent(route.query.callback) : '/'
|
||||||
|
await navigateTo(callback)
|
||||||
}
|
}
|
||||||
submitting.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'vuetify-app'
|
layout: 'vuetify-app'
|
||||||
})
|
})
|
||||||
|
|
||||||
const { $auth } = useNuxtApp()
|
|
||||||
|
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
password1: '',
|
password1: '',
|
||||||
password2: '',
|
password2: '',
|
||||||
|
code:'',
|
||||||
})
|
})
|
||||||
|
|
||||||
const fieldErrors = ref({
|
const fieldErrors = ref({
|
||||||
@@ -17,26 +17,30 @@ const fieldErrors = ref({
|
|||||||
email: '',
|
email: '',
|
||||||
password1: '',
|
password1: '',
|
||||||
password2: '',
|
password2: '',
|
||||||
|
code:'',
|
||||||
})
|
})
|
||||||
|
|
||||||
const formRules = ref({
|
const formRules = ref({
|
||||||
username: [
|
username: [
|
||||||
v => !!v || 'Please enter your username',
|
v => !!v || $i18n.t('Please enter your username'),
|
||||||
v => v.length >= 4 || 'Username must be at least 4 characters'
|
v => v.length >= 4 || $i18n.t('Username must be at least 4 characters')
|
||||||
],
|
],
|
||||||
email: [
|
email: [
|
||||||
v => !!v || 'Please enter your e-mail address',
|
v => !!v || $i18n.t('Please enter your e-mail address'),
|
||||||
v => /.+@.+\..+/.test(v) || 'E-mail address must be valid'
|
v => /.+@.+\..+/.test(v) || $i18n.t('E-mail address must be valid')
|
||||||
],
|
],
|
||||||
password1: [
|
password1: [
|
||||||
v => !!v || 'Please enter your password',
|
v => !!v || $i18n.t('Please enter your password'),
|
||||||
v => v.length >= 8 || 'Password must be at least 8 characters'
|
v => v.length >= 8 || $i18n.t('Password must be at least 8 characters')
|
||||||
],
|
],
|
||||||
password2: [
|
password2: [
|
||||||
v => !!v || 'Please confirm your password',
|
v => !!v || $i18n.t('Please confirm your password'),
|
||||||
v => v.length >= 8 || 'Password must be at least 8 characters',
|
v => v.length >= 8 || $i18n.t('Password must be at least 8 characters'),
|
||||||
v => v === formData.value.password1 || 'Confirm password must match password'
|
v => v === formData.value.password1 || $i18n.t('Confirm password must match password')
|
||||||
]
|
],
|
||||||
|
code: [
|
||||||
|
v => !!v || $i18n.t('Please enter your code'),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
@@ -60,21 +64,21 @@ const submit = async () => {
|
|||||||
if (error.value.status === 400) {
|
if (error.value.status === 400) {
|
||||||
for (const key in formData.value) {
|
for (const key in formData.value) {
|
||||||
if (error.value.data[key]) {
|
if (error.value.data[key]) {
|
||||||
fieldErrors.value[key] = error.value.data[key][0]
|
fieldErrors.value[key] = $i18n.t(error.value.data[key][0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (error.value.data.non_field_errors) {
|
if (error.value.data.non_field_errors) {
|
||||||
errorMsg.value = error.value.data.non_field_errors[0]
|
errorMsg.value = $i18n.t(error.value.data.non_field_errors[0])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (error.value.data.detail) {
|
if (error.value.data.detail) {
|
||||||
errorMsg.value = error.value.data.detail
|
errorMsg.value = $i18n.t(error.value.data.detail)
|
||||||
} else {
|
} else {
|
||||||
errorMsg.value = 'Something went wrong. Please try again.'
|
errorMsg.value = 'Something went wrong. Please try again.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$auth.setUser(data.value.user)
|
setUser(data.value.user)
|
||||||
navigateTo('/account/onboarding?email_verification_required='+data.value.email_verification_required)
|
navigateTo('/account/onboarding?email_verification_required='+data.value.email_verification_required)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +87,7 @@ const submit = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleFieldUpdate = (field) => {
|
const handleFieldUpdate = (field) => {
|
||||||
// fieldErrors.value[field] = ''
|
fieldErrors.value[field] = ''
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -103,14 +107,14 @@ const handleFieldUpdate = (field) => {
|
|||||||
class="mt-15"
|
class="mt-15"
|
||||||
elevation="0"
|
elevation="0"
|
||||||
>
|
>
|
||||||
<div class="text-center text-h4">Create your account</div>
|
<div class="text-center text-h4">{{$t('Create your account')}}</div>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-form ref="signUpForm" class="mt-5">
|
<v-form ref="signUpForm" class="mt-5">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="formData.username"
|
v-model="formData.username"
|
||||||
:rules="formRules.username"
|
:rules="formRules.username"
|
||||||
:error-messages="fieldErrors.username"
|
:error-messages="fieldErrors.username"
|
||||||
label="User name"
|
:label="$t('username')"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@update:modelValue="handleFieldUpdate('username')"
|
@update:modelValue="handleFieldUpdate('username')"
|
||||||
clearable
|
clearable
|
||||||
@@ -120,9 +124,9 @@ const handleFieldUpdate = (field) => {
|
|||||||
v-model="formData.email"
|
v-model="formData.email"
|
||||||
:rules="formRules.email"
|
:rules="formRules.email"
|
||||||
:error-messages="fieldErrors.email"
|
:error-messages="fieldErrors.email"
|
||||||
label="Email"
|
:label="$t('email')"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@@update:modelValue="handleFieldUpdate('email')"
|
@update:modelValue="handleFieldUpdate('email')"
|
||||||
clearable
|
clearable
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
@@ -130,7 +134,7 @@ const handleFieldUpdate = (field) => {
|
|||||||
v-model="formData.password1"
|
v-model="formData.password1"
|
||||||
:rules="formRules.password1"
|
:rules="formRules.password1"
|
||||||
:error-messages="fieldErrors.password1"
|
:error-messages="fieldErrors.password1"
|
||||||
label="Password"
|
:label="$t('password')"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@update:modelValue="handleFieldUpdate('password1')"
|
@update:modelValue="handleFieldUpdate('password1')"
|
||||||
clearable
|
clearable
|
||||||
@@ -140,12 +144,21 @@ const handleFieldUpdate = (field) => {
|
|||||||
v-model="formData.password2"
|
v-model="formData.password2"
|
||||||
:rules="formRules.password2"
|
:rules="formRules.password2"
|
||||||
:error-messages="fieldErrors.password2"
|
:error-messages="fieldErrors.password2"
|
||||||
label="Confirm password"
|
:label="$t('confirmPassword')"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@update:modelValue="handleFieldUpdate('password2')"
|
@update:modelValue="handleFieldUpdate('password2')"
|
||||||
clearable
|
clearable
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.code"
|
||||||
|
:rules="formRules.code"
|
||||||
|
:label="$t('invitation code')"
|
||||||
|
variant="underlined"
|
||||||
|
@keyup.enter="submit"
|
||||||
|
clearable
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
</v-form>
|
</v-form>
|
||||||
|
|
||||||
<div v-if="errorMsg" class="text-red">{{ errorMsg }}</div>
|
<div v-if="errorMsg" class="text-red">{{ errorMsg }}</div>
|
||||||
@@ -157,14 +170,14 @@ const handleFieldUpdate = (field) => {
|
|||||||
@click="navigateTo('/account/signin')"
|
@click="navigateTo('/account/signin')"
|
||||||
variant="text"
|
variant="text"
|
||||||
color="primary"
|
color="primary"
|
||||||
>Sign in instead</v-btn>
|
>{{$t('Sign in instead')}}</v-btn>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
size="large"
|
size="large"
|
||||||
color="primary"
|
color="primary"
|
||||||
:loading="submitting"
|
:loading="submitting"
|
||||||
@click="submit"
|
@click="submit"
|
||||||
>Submit</v-btn>
|
>{{$t('signUp')}}</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|||||||
292
pages/index.vue
292
pages/index.vue
@@ -1,267 +1,91 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Prompt from "~/components/Prompt.vue";
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ["auth"]
|
middleware: ["auth"],
|
||||||
|
path: '/:id?',
|
||||||
|
keepalive: true
|
||||||
})
|
})
|
||||||
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
|
|
||||||
|
|
||||||
const { $i18n, $auth } = useNuxtApp()
|
const { $i18n } = useNuxtApp()
|
||||||
const runtimeConfig = useRuntimeConfig()
|
const runtimeConfig = useRuntimeConfig()
|
||||||
const currentModel = useCurrentModel()
|
const drawer = useDrawer()
|
||||||
const openaiApiKey = useApiKey()
|
const route = useRoute()
|
||||||
const fetchingResponse = ref(false)
|
const conversation = ref(getDefaultConversationData())
|
||||||
const messageQueue = []
|
|
||||||
let isProcessingQueue = false
|
|
||||||
|
|
||||||
const processMessageQueue = () => {
|
const loadConversation = async () => {
|
||||||
if (isProcessingQueue || messageQueue.length === 0) {
|
const { data, error } = await useAuthFetch('/api/chat/conversations/' + route.params.id)
|
||||||
return
|
if (!error.value) {
|
||||||
}
|
conversation.value = Object.assign(conversation.value, data.value)
|
||||||
if (!currentConversation.value.messages[currentConversation.value.messages.length - 1].is_bot) {
|
|
||||||
currentConversation.value.messages.push({id: null, is_bot: true, message: ''})
|
|
||||||
}
|
|
||||||
isProcessingQueue = true
|
|
||||||
const nextMessage = messageQueue.shift()
|
|
||||||
if (runtimeConfig.public.typewriter) {
|
|
||||||
let wordIndex = 0;
|
|
||||||
const intervalId = setInterval(() => {
|
|
||||||
currentConversation.value.messages[currentConversation.value.messages.length - 1].message += nextMessage[wordIndex]
|
|
||||||
wordIndex++
|
|
||||||
if (wordIndex === nextMessage.length) {
|
|
||||||
clearInterval(intervalId)
|
|
||||||
isProcessingQueue = false
|
|
||||||
processMessageQueue()
|
|
||||||
}
|
|
||||||
}, runtimeConfig.public.typewriterDelay)
|
|
||||||
} else {
|
|
||||||
currentConversation.value.messages[currentConversation.value.messages.length - 1].message += nextMessage
|
|
||||||
isProcessingQueue = false
|
|
||||||
processMessageQueue()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let ctrl
|
const loadMessage = async () => {
|
||||||
const abortFetch = () => {
|
const { data, error } = await useAuthFetch('/api/chat/messages/?conversationId=' + route.params.id)
|
||||||
if (ctrl) {
|
if (!error.value) {
|
||||||
ctrl.abort()
|
conversation.value.messages = data.value
|
||||||
}
|
|
||||||
fetchingResponse.value = false
|
|
||||||
}
|
|
||||||
const fetchReply = async (message) => {
|
|
||||||
ctrl = new AbortController()
|
|
||||||
|
|
||||||
let webSearchParams = {}
|
|
||||||
if (enableWebSearch.value) {
|
|
||||||
webSearchParams['web_search'] = {
|
|
||||||
ua: navigator.userAgent,
|
|
||||||
default_prompt: $i18n.t('webSearchDefaultPrompt')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = Object.assign({}, currentModel.value, {
|
const createNewConversation = () => {
|
||||||
openaiApiKey: openaiApiKey.value,
|
if (route.path !== '/') {
|
||||||
message: message,
|
return navigateTo('/?new')
|
||||||
conversationId: currentConversation.value.id
|
|
||||||
}, webSearchParams)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fetchEventSource('/api/conversation/', {
|
|
||||||
signal: ctrl.signal,
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
onopen(response) {
|
|
||||||
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
throw new Error(`Failed to send message. HTTP ${response.status} - ${response.statusText}`);
|
conversation.value = Object.assign(getDefaultConversationData(), {
|
||||||
},
|
topic: $i18n.t('newConversation')
|
||||||
onclose() {
|
|
||||||
if (ctrl.signal.aborted === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to send message. Server closed the connection unexpectedly.`);
|
|
||||||
},
|
|
||||||
onerror(err) {
|
|
||||||
throw err;
|
|
||||||
},
|
|
||||||
async onmessage(message) {
|
|
||||||
// console.log(message)
|
|
||||||
const event = message.event
|
|
||||||
const data = JSON.parse(message.data)
|
|
||||||
|
|
||||||
if (event === 'error') {
|
|
||||||
throw new Error(data.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event === 'userMessageId') {
|
|
||||||
currentConversation.value.messages[currentConversation.value.messages.length - 1].id = data.userMessageId
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event === 'done') {
|
|
||||||
if (currentConversation.value.id === null) {
|
|
||||||
currentConversation.value.id = data.conversationId
|
|
||||||
genTitle(currentConversation.value.id)
|
|
||||||
}
|
|
||||||
currentConversation.value.messages[currentConversation.value.messages.length - 1].id = data.messageId
|
|
||||||
abortFetch()
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
messageQueue.push(data.content)
|
|
||||||
processMessageQueue()
|
|
||||||
|
|
||||||
scrollChatWindow()
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
abortFetch()
|
|
||||||
showSnackbar(err.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentConversation = useConversion()
|
|
||||||
|
|
||||||
const grab = ref(null)
|
|
||||||
const scrollChatWindow = () => {
|
|
||||||
if (grab.value === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
grab.value.scrollIntoView({behavior: 'smooth'})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const send = (message) => {
|
onMounted(async () => {
|
||||||
fetchingResponse.value = true
|
if (route.params.id) {
|
||||||
currentConversation.value.messages.push({message: message})
|
conversation.value.loadingMessages = true
|
||||||
fetchReply(message)
|
await loadConversation()
|
||||||
scrollChatWindow()
|
await loadMessage()
|
||||||
|
conversation.value.loadingMessages = false
|
||||||
}
|
}
|
||||||
const stop = () => {
|
})
|
||||||
abortFetch()
|
|
||||||
|
|
||||||
|
const navTitle = computed(() => {
|
||||||
|
if (conversation.value && conversation.value.topic !== null) {
|
||||||
|
return conversation.value.topic === '' ? $i18n.t('defaultConversationTitle') : conversation.value.topic
|
||||||
}
|
}
|
||||||
|
return runtimeConfig.public.appName
|
||||||
|
})
|
||||||
|
|
||||||
const snackbar = ref(false)
|
onActivated(async () => {
|
||||||
const snackbarText = ref('')
|
if (route.path === '/' && route.query.new !== undefined) {
|
||||||
const showSnackbar = (text) => {
|
createNewConversation()
|
||||||
snackbarText.value = text
|
|
||||||
snackbar.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const editor = ref(null)
|
|
||||||
const usePrompt = (prompt) => {
|
|
||||||
editor.value.usePrompt(prompt)
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteMessage = (index) => {
|
|
||||||
currentConversation.value.messages.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const showWebSearchToggle = ref(false)
|
|
||||||
const enableWebSearch = ref(false)
|
|
||||||
|
|
||||||
const settings = useSettings()
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
if (settings.value) {
|
|
||||||
const settingsValue = toRaw(settings.value)
|
|
||||||
showWebSearchToggle.value = settingsValue.open_web_search && settingsValue.open_web_search === 'True'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<v-app-bar>
|
||||||
v-if="currentConversation.messages.length > 0"
|
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
|
||||||
ref="chatWindow"
|
|
||||||
>
|
<v-toolbar-title>{{ navTitle }}</v-toolbar-title>
|
||||||
<v-container>
|
|
||||||
<v-row>
|
|
||||||
<v-col
|
|
||||||
v-for="(message, index) in currentConversation.messages" :key="index"
|
|
||||||
cols="12"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="d-flex align-center"
|
|
||||||
:class="message.is_bot ? 'justify-start' : 'justify-end'"
|
|
||||||
>
|
|
||||||
<MessageActions
|
|
||||||
v-if="!message.is_bot"
|
|
||||||
:message="message"
|
|
||||||
:message-index="index"
|
|
||||||
:use-prompt="usePrompt"
|
|
||||||
:delete-message="deleteMessage"
|
|
||||||
/>
|
|
||||||
<MsgContent :message="message" />
|
|
||||||
<MessageActions
|
|
||||||
v-if="message.is_bot"
|
|
||||||
:message="message"
|
|
||||||
:message-index="index"
|
|
||||||
:use-prompt="usePrompt"
|
|
||||||
:delete-message="deleteMessage"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-container>
|
|
||||||
|
|
||||||
<div ref="grab" class="w-100" style="height: 200px;"></div>
|
|
||||||
</div>
|
|
||||||
<Welcome v-else />
|
|
||||||
<v-footer app>
|
|
||||||
<div class="px-md-16 w-100 d-flex flex-column">
|
|
||||||
<div class="d-flex align-center">
|
|
||||||
<v-btn
|
|
||||||
v-show="fetchingResponse"
|
|
||||||
icon="close"
|
|
||||||
title="stop"
|
|
||||||
class="mr-3"
|
|
||||||
@click="stop"
|
|
||||||
></v-btn>
|
|
||||||
<MsgEditor ref="editor" :send-message="send" :disabled="fetchingResponse" :loading="fetchingResponse" />
|
|
||||||
</div>
|
|
||||||
<v-toolbar
|
|
||||||
density="comfortable"
|
|
||||||
color="transparent"
|
|
||||||
>
|
|
||||||
<Prompt v-show="!fetchingResponse" :use-prompt="usePrompt" />
|
|
||||||
<v-switch
|
|
||||||
v-if="showWebSearchToggle"
|
|
||||||
v-model="enableWebSearch"
|
|
||||||
hide-details
|
|
||||||
color="primary"
|
|
||||||
:label="$t('webSearch')"
|
|
||||||
></v-switch>
|
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
</v-toolbar>
|
|
||||||
|
|
||||||
<!-- <div class="py-2 text-disabled text-caption font-weight-light text-center">-->
|
|
||||||
<!-- © {{ new Date().getFullYear() }} {{ runtimeConfig.public.appName }}-->
|
|
||||||
<!-- </div>-->
|
|
||||||
</div>
|
|
||||||
</v-footer>
|
|
||||||
<v-snackbar
|
|
||||||
v-model="snackbar"
|
|
||||||
multi-line
|
|
||||||
location="top"
|
|
||||||
>
|
|
||||||
{{ snackbarText }}
|
|
||||||
|
|
||||||
<template v-slot:actions>
|
|
||||||
<v-btn
|
<v-btn
|
||||||
color="red"
|
:title="$t('newConversation')"
|
||||||
variant="text"
|
icon="add"
|
||||||
@click="snackbar = false"
|
@click="createNewConversation"
|
||||||
|
class="d-md-none"
|
||||||
|
></v-btn>
|
||||||
|
<v-btn
|
||||||
|
variant="outlined"
|
||||||
|
class="text-none d-none d-md-block"
|
||||||
|
@click="createNewConversation"
|
||||||
>
|
>
|
||||||
Close
|
{{ $t('newConversation') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
|
||||||
</v-snackbar>
|
</v-app-bar>
|
||||||
|
|
||||||
|
<v-main>
|
||||||
|
<Welcome v-if="!route.params.id && conversation.messages.length === 0" />
|
||||||
|
<Conversation :conversation="conversation" />
|
||||||
|
</v-main>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
|
|
||||||
const AUTH_ROUTE = {
|
|
||||||
home: '/',
|
|
||||||
login: '/account/signin',
|
|
||||||
}
|
|
||||||
|
|
||||||
const ENDPOINTS = {
|
|
||||||
login: {
|
|
||||||
url: '/api/account/login/'
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
url: '/api/account/user/'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
|
||||||
|
|
||||||
class Auth {
|
|
||||||
constructor() {
|
|
||||||
this.loginIn = useState('loginIn', () => false)
|
|
||||||
this.user = useState('user')
|
|
||||||
}
|
|
||||||
|
|
||||||
async logout () {
|
|
||||||
this.loginIn.value = false
|
|
||||||
this.user.value = null
|
|
||||||
await this.redirectToLogin()
|
|
||||||
}
|
|
||||||
|
|
||||||
setUser (user) {
|
|
||||||
this.user = user
|
|
||||||
this.loginIn.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchUser () {
|
|
||||||
const { data, error } = await useFetch(ENDPOINTS.user.url, {
|
|
||||||
// withCredentials: true
|
|
||||||
})
|
|
||||||
if (!error.value) {
|
|
||||||
this.setUser(data.value)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
|
|
||||||
async redirectToLogin (callback) {
|
|
||||||
return await navigateTo(
|
|
||||||
AUTH_ROUTE.login + '?callback=' + encodeURIComponent(callback || AUTH_ROUTE.home)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const auth = new Auth()
|
|
||||||
|
|
||||||
addRouteMiddleware('auth', async (to, from) => {
|
|
||||||
if (!auth.loginIn.value) {
|
|
||||||
const error = await auth.fetchUser()
|
|
||||||
if (error) {
|
|
||||||
return await auth.redirectToLogin(to.fullPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
provide: {
|
|
||||||
auth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
6
plugins/initApiKey.js
Normal file
6
plugins/initApiKey.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
nuxtApp.hook('app:created', async () => {
|
||||||
|
const apiKey = useApiKey()
|
||||||
|
apiKey.value = getStoredApiKey()
|
||||||
|
})
|
||||||
|
})
|
||||||
24
plugins/settings.js
Normal file
24
plugins/settings.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const transformData = (list) => {
|
||||||
|
const result = {};
|
||||||
|
for (let i = 0; i < list.length; i++) {
|
||||||
|
const item = list[i];
|
||||||
|
result[item.name] = item.value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
nuxtApp.hook('app:created', async () => {
|
||||||
|
let settings = {}
|
||||||
|
|
||||||
|
const { data, error } = await useAuthFetch('/api/chat/settings/', {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!error.value) {
|
||||||
|
settings = transformData(data.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
nuxtApp.provide('settings', settings)
|
||||||
|
})
|
||||||
|
})
|
||||||
14
server/middleware/apiProxy.ts
Normal file
14
server/middleware/apiProxy.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { createProxyMiddleware } from 'http-proxy-middleware'
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
createProxyMiddleware({
|
||||||
|
target: process.env.SERVER_DOMAIN,
|
||||||
|
pathFilter: '/api',
|
||||||
|
})(event.node.req, event.node.res, (err) => {
|
||||||
|
if (err)
|
||||||
|
reject(err)
|
||||||
|
else
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
22
static.Dockerfile
Normal file
22
static.Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
FROM node:18-alpine3.16 as builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
|
||||||
|
RUN yarn install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN rm -r server && SSR=false yarn generate
|
||||||
|
|
||||||
|
|
||||||
|
FROM nginx:1.22-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/.output/public .
|
||||||
|
|
||||||
|
COPY nginx.conf /etc/nginx/templates/default.conf.template
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
@@ -5,11 +5,25 @@ export const STORAGE_KEY = {
|
|||||||
OPENAI_API_KEY: 'openai_api_key',
|
OPENAI_API_KEY: 'openai_api_key',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_MODEL = {
|
export const MODELS = {
|
||||||
|
'gpt-3.5-turbo': {
|
||||||
name: 'gpt-3.5-turbo',
|
name: 'gpt-3.5-turbo',
|
||||||
frequency_penalty: 0.0,
|
frequency_penalty: 0.0,
|
||||||
presence_penalty: 0.0,
|
presence_penalty: 0.0,
|
||||||
|
total_tokens: 4096,
|
||||||
max_tokens: 1000,
|
max_tokens: 1000,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
top_p: 1.0
|
top_p: 1.0
|
||||||
|
},
|
||||||
|
'gpt-4': {
|
||||||
|
name: 'gpt-4',
|
||||||
|
frequency_penalty: 0.0,
|
||||||
|
presence_penalty: 0.0,
|
||||||
|
total_tokens: 8192,
|
||||||
|
max_tokens: 2000,
|
||||||
|
temperature: 0.7,
|
||||||
|
top_p: 1.0
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_MODEL_NAME = 'gpt-3.5-turbo'
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
export const getDefaultConversionData = () => {
|
export const getDefaultConversationData = () => {
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
return {
|
return {
|
||||||
id: null,
|
id: null,
|
||||||
topic: null,
|
topic: null,
|
||||||
@@ -8,7 +9,7 @@ export const getDefaultConversionData = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getConversions = async () => {
|
export const getConversations = async () => {
|
||||||
const { data, error } = await useAuthFetch('/api/chat/conversations/')
|
const { data, error } = await useAuthFetch('/api/chat/conversations/')
|
||||||
if (!error.value) {
|
if (!error.value) {
|
||||||
return data.value
|
return data.value
|
||||||
@@ -16,57 +17,46 @@ export const getConversions = async () => {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createNewConversion = () => {
|
export const addConversation = (conversation) => {
|
||||||
const conversation = useConversion()
|
const conversations = useConversations()
|
||||||
conversation.value = getDefaultConversionData()
|
conversations.value = [conversation, ...conversations.value]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const openConversationMessages = async (currentConversation) => {
|
|
||||||
const conversation = useConversion()
|
|
||||||
conversation.value = Object.assign(conversation.value, currentConversation)
|
|
||||||
conversation.value.loadingMessages = true
|
|
||||||
const { data, error } = await useAuthFetch('/api/chat/messages/?conversationId=' + currentConversation.id)
|
|
||||||
if (!error.value) {
|
|
||||||
conversation.value.messages = data.value
|
|
||||||
}
|
|
||||||
conversation.value.loadingMessages = true
|
|
||||||
}
|
|
||||||
|
|
||||||
export const genTitle = async (conversationId) => {
|
export const genTitle = async (conversationId) => {
|
||||||
|
const { $i18n, $settings } = useNuxtApp()
|
||||||
|
const openaiApiKey = useApiKey()
|
||||||
const { data, error } = await useAuthFetch('/api/gen_title/', {
|
const { data, error } = await useAuthFetch('/api/gen_title/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
conversationId: conversationId
|
conversationId: conversationId,
|
||||||
|
prompt: $i18n.t('genTitlePrompt'),
|
||||||
|
openaiApiKey: $settings.open_api_key_setting === 'True' ? openaiApiKey.value : null,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (!error.value) {
|
if (!error.value) {
|
||||||
const conversation = {
|
const conversations = useConversations()
|
||||||
id: conversationId,
|
let index = conversations.value.findIndex(item => item.id === conversationId)
|
||||||
topic: data.value.title,
|
if (index === -1) {
|
||||||
|
index = 0
|
||||||
}
|
}
|
||||||
const conversations = useConversions()
|
conversations.value[index].topic = data.value.title
|
||||||
// prepend to conversations
|
|
||||||
conversations.value = [conversation, ...conversations.value]
|
|
||||||
return data.value.title
|
return data.value.title
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformData = (list) => {
|
export const fetchUser = async () => {
|
||||||
const result = {};
|
return useMyFetch('/api/account/user/')
|
||||||
for (let i = 0; i < list.length; i++) {
|
|
||||||
const item = list[i];
|
|
||||||
result[item.name] = item.value;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadSettings = async () => {
|
export const setUser = (userData) => {
|
||||||
const settings = useSettings()
|
const user = useUser()
|
||||||
const { data, error } = await useAuthFetch('/api/chat/settings/', {
|
user.value = userData
|
||||||
method: 'GET'
|
|
||||||
})
|
|
||||||
if (!error.value) {
|
|
||||||
settings.value = transformData(data.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const logout = () => {
|
||||||
|
const user = useUser()
|
||||||
|
user.value = null
|
||||||
|
return navigateTo('/account/signin');
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
const get = (key) => {
|
const get = (key) => {
|
||||||
|
if (process.server) return
|
||||||
let val = localStorage.getItem(key)
|
let val = localStorage.getItem(key)
|
||||||
if (val) {
|
if (val) {
|
||||||
val = JSON.parse(val)
|
val = JSON.parse(val)
|
||||||
@@ -8,6 +9,7 @@ const get = (key) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const set = (key, val) => {
|
const set = (key, val) => {
|
||||||
|
if (process.server) return
|
||||||
localStorage.setItem(key, JSON.stringify(val))
|
localStorage.setItem(key, JSON.stringify(val))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,13 +19,13 @@ export const setModels = (val) => {
|
|||||||
models.value = val
|
models.value = val
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStoredModels = () => {
|
// export const getStoredModels = () => {
|
||||||
let models = get(STORAGE_KEY.MODELS)
|
// let models = get(STORAGE_KEY.MODELS)
|
||||||
if (!models) {
|
// if (!models) {
|
||||||
models = [DEFAULT_MODEL]
|
// models = [DEFAULT_MODEL]
|
||||||
}
|
// }
|
||||||
return models
|
// return models
|
||||||
}
|
// }
|
||||||
|
|
||||||
export const saveCurrentModel = (val) => {
|
export const saveCurrentModel = (val) => {
|
||||||
set(STORAGE_KEY.CURRENT_MODEL, val)
|
set(STORAGE_KEY.CURRENT_MODEL, val)
|
||||||
@@ -32,7 +34,7 @@ export const saveCurrentModel = (val) => {
|
|||||||
export const getCurrentModel = () => {
|
export const getCurrentModel = () => {
|
||||||
let model = get(STORAGE_KEY.CURRENT_MODEL)
|
let model = get(STORAGE_KEY.CURRENT_MODEL)
|
||||||
if (!model) {
|
if (!model) {
|
||||||
model = DEFAULT_MODEL
|
model = MODELS[DEFAULT_MODEL_NAME]
|
||||||
}
|
}
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user