Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
323f10844b | ||
|
|
ee035390db | ||
|
|
be743bf799 | ||
|
|
a59f84f2bf | ||
|
|
ed0cf2997d | ||
|
|
7f00c74097 | ||
|
|
f007417fa4 | ||
|
|
27c5e2a3ac | ||
|
|
e90dc0c12b | ||
|
|
837fd8c9ff | ||
|
|
ce0b1004f3 | ||
|
|
1ff1c46e37 | ||
|
|
afa3e499dc | ||
|
|
70ce5746bc | ||
|
|
35d4292d29 | ||
|
|
8bbc44e7bf | ||
|
|
3dcb4be6e4 | ||
|
|
83f8072625 | ||
|
|
3992121b71 | ||
|
|
d08806f0c9 | ||
|
|
85ac73efcc | ||
|
|
7cc5a6b347 | ||
|
|
983e4d436d | ||
|
|
727826f1b1 | ||
|
|
386659109c | ||
|
|
bd9e8bf45e | ||
|
|
4e40530a8c | ||
|
|
ea69a350f4 | ||
|
|
18a4251714 | ||
|
|
878fda0054 | ||
|
|
1f3a025918 | ||
|
|
f9db3e5866 | ||
|
|
c9615ed05c | ||
|
|
0d4b6247e2 | ||
|
|
c9c3431cff | ||
|
|
46abf3daa0 | ||
|
|
8dcd7f46b1 | ||
|
|
33d9c392fa | ||
|
|
bb17cdd123 | ||
|
|
4cfc9f4aea | ||
|
|
cd50086c1e | ||
|
|
7e5498f779 | ||
|
|
d933236a5d | ||
|
|
0be2d45cd5 | ||
|
|
e24ad26d99 | ||
|
|
052f5299a0 | ||
|
|
8340edbf40 | ||
|
|
7bff84638e | ||
|
|
54660706e3 | ||
|
|
a8acfeea58 | ||
|
|
85fc57e2b2 | ||
|
|
fe4740b7a2 | ||
|
|
2210dfcb98 | ||
|
|
19794016fd | ||
|
|
ce348c0f38 | ||
|
|
f251b16afe | ||
|
|
4f32ef69b2 | ||
|
|
e354a9490f | ||
|
|
3d2c041cc2 | ||
|
|
17588443e6 |
@@ -1,4 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
database.sqlite
|
dist
|
||||||
.idea
|
.idea
|
||||||
|
.output
|
||||||
|
.nuxt
|
||||||
.env
|
.env
|
||||||
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
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,4 +7,3 @@ node_modules
|
|||||||
.env
|
.env
|
||||||
.idea
|
.idea
|
||||||
dist
|
dist
|
||||||
database.sqlite
|
|
||||||
|
|||||||
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"]
|
||||||
85
README.md
85
README.md
@@ -1,14 +1,47 @@
|
|||||||
<p align="center">
|
<div align="center">
|
||||||
<img alt="demo" src="./demos/demo.png?v=1">
|
<h1>ChatGPT UI</h1>
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
# ChatGPT UI
|
[English](./README.md) | [中文](./docs/zh/README.md)
|
||||||
|
|
||||||
|
User guide: [https://wongsaang.github.io/chatgpt-ui-docs/](https://wongsaang.github.io/chatgpt-ui-docs/)
|
||||||
|
|
||||||
|
A ChatGPT web client that supports multiple users, multiple database connections for persistent data storage, supports i18n. Provides Docker images and quick deployment scripts.
|
||||||
|
|
||||||
|
The server of this project:[https://github.com/WongSaang/chatgpt-ui-server](https://github.com/WongSaang/chatgpt-ui-server)
|
||||||
|
|
||||||
|
https://user-images.githubusercontent.com/46235412/227156264-ca17ab17-999b-414f-ab06-3f75b5235bfe.mp4
|
||||||
|
|
||||||
A web client for ChatGPT, using OpenAI's API. Provides Docker images and also supports quick deployment to servers using shell scripts.
|
|
||||||
|
|
||||||
## 📢Updates
|
## 📢Updates
|
||||||
|
|
||||||
<details open>
|
<details open>
|
||||||
|
<summary><strong>2023-04-06</strong></summary>
|
||||||
|
The client is now deployed as server-side rendering (SSR), and the environment variables are now available, see docker-compose configuration below for available environment variables. Improved first screen loading speed and reduced white screen time.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary><strong>2023-03-27</strong></summary>
|
||||||
|
🚀 Support gpt-4 model. You can select the model in the "Model Parameters" of the front-end.
|
||||||
|
The GPT-4 model requires whitelist access from OpenAI.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<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>
|
||||||
<summary><strong>2023-03-04</strong></summary>
|
<summary><strong>2023-03-04</strong></summary>
|
||||||
|
|
||||||
**Update to the latest official chat model** `gpt-3.5-turbo`
|
**Update to the latest official chat model** `gpt-3.5-turbo`
|
||||||
@@ -17,22 +50,22 @@ A web client for ChatGPT, using OpenAI's API. Provides Docker images and also su
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details open>
|
<details>
|
||||||
|
|
||||||
<summary><strong>2023-02-24</strong></summary>
|
<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).
|
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).
|
If you still wish to use the old version, please visit the [v1 branch](https://github.com/WongSaang/chatgpt-ui/tree/v1).
|
||||||
|
|
||||||
Version 2 introduces the following new features:
|
</details>
|
||||||
|
|
||||||
|
## Version 2 introduces the following new features:
|
||||||
|
|
||||||
- 😉 Separation of the frontend and backend, with the backend now using the Python-based Django framework.
|
- 😉 Separation of the frontend and backend, with the backend now using the Python-based Django framework.
|
||||||
- 😘 User authentication, supporting multiple users.
|
- 😘 User authentication, supporting multiple users.
|
||||||
- 😀 Ability to store data in an external database (defaulting to Sqlite).
|
- 😀 Ability to store data in an external database (defaulting to Sqlite).
|
||||||
- 😎 Session persistence, allowing the API to answer questions based on your context.
|
- 😎 Session persistence, allowing the API to answer questions based on your context.
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## 🚀 One-click deployment <a name="one-click-depolyment"></a>
|
## 🚀 One-click deployment <a name="one-click-depolyment"></a>
|
||||||
|
|
||||||
Note: This script has only been tested on Ubuntu Server 22.04 LTS.
|
Note: This script has only been tested on Ubuntu Server 22.04 LTS.
|
||||||
@@ -71,6 +104,9 @@ services:
|
|||||||
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' # 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:
|
depends_on:
|
||||||
- backend-web-server
|
- backend-web-server
|
||||||
ports:
|
ports:
|
||||||
@@ -81,17 +117,20 @@ services:
|
|||||||
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||||
environment:
|
environment:
|
||||||
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000} # CSRF whitelist,Add the address of your chatgpt-ui-web-server here, default is localhost:9000
|
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000} # CSRF whitelist,Add the address of your chatgpt-ui-web-server here, default is localhost:9000
|
||||||
|
- SERVER_WORKERS=3 # Number of gunicorn workers, default is 3
|
||||||
#- 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.
|
#- 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 # If you are in China, you can use the proxy provided by the author to speed up the connection to the OpenAI API. If you do not need to use the proxy, you can delete this parameter.
|
#- OPENAI_API_PROXY=https://openai.proxy.com/v1 # Proxy for https://api.openai.com/v1
|
||||||
- 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
|
||||||
|
- 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
|
# If you want to use the email verification function, you need to configure the following parameters
|
||||||
# - EMAIL_HOST=SMTP server address
|
# - EMAIL_HOST=SMTP server address
|
||||||
# - EMAIL_PORT=SMTP server port
|
# - EMAIL_PORT=SMTP server port
|
||||||
# - EMAIL_HOST_USER=
|
# - EMAIL_HOST_USER=
|
||||||
# - EMAIL_HOST_PASSWORD=
|
# - EMAIL_HOST_PASSWORD=
|
||||||
# - EMAIL_USE_TLS=True
|
# - EMAIL_USE_TLS=True
|
||||||
|
# - EMAIL_FROM=no-reply@example.com #Default sender email address
|
||||||
ports:
|
ports:
|
||||||
- '8000:8000'
|
- '8000:8000'
|
||||||
networks:
|
networks:
|
||||||
@@ -112,16 +151,36 @@ networks:
|
|||||||
driver: bridge
|
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
|
### Set API key
|
||||||
|
|
||||||
After running the services, you can access the web client at `http://localhost`, and an admin panel at `http://localhost:9000/admin`.
|
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 superuser: `admin`
|
||||||
|
|
||||||
Default password: `password`
|
Default password: `password`
|
||||||
|
|
||||||
Before you can start chatting, you need to log in to the admin panel 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.
|
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
|
## Development
|
||||||
|
|
||||||
@@ -148,4 +207,4 @@ Build the application for production:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn build
|
yarn build
|
||||||
```
|
```
|
||||||
|
|||||||
15
app.vue
Normal file
15
app.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup>
|
||||||
|
onNuxtReady(() => {
|
||||||
|
fetchSystemSettings()
|
||||||
|
// api key
|
||||||
|
const apiKey = useApiKey()
|
||||||
|
apiKey.value = getStoredApiKey()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtLoadingIndicator />
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</template>
|
||||||
328
components/Conversation.vue
Normal file
328
components/Conversation.vue
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
<script setup>
|
||||||
|
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
|
||||||
|
|
||||||
|
const { $i18n } = 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: enableCustomApiKey.value ? 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 settings = useSettings()
|
||||||
|
const enableWebSearch = ref(false)
|
||||||
|
|
||||||
|
const showWebSearchToggle = computed(() => {
|
||||||
|
return settings.value && settings.value.open_web_search && settings.value.open_web_search === 'True'
|
||||||
|
})
|
||||||
|
|
||||||
|
const enableCustomApiKey = computed(() => {
|
||||||
|
return settings.value && settings.value.open_api_key_setting && settings.value.open_api_key_setting === 'True'
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
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="showWebSearchToggle"
|
||||||
|
v-model="enableWebSearch"
|
||||||
|
inline
|
||||||
|
hide-details
|
||||||
|
color="primary"
|
||||||
|
:label="$t('webSearch')"
|
||||||
|
></v-switch>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<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"
|
||||||
|
></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>
|
||||||
|
</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>
|
||||||
98
components/MessageActions.vue
Normal file
98
components/MessageActions.vue
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<script setup>
|
||||||
|
import copy from 'copy-to-clipboard'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
message: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
messageIndex: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
usePrompt: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
deleteMessage: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const snackbar = ref(false)
|
||||||
|
const snackbarText = ref('')
|
||||||
|
const showSnackbar = (text) => {
|
||||||
|
snackbarText.value = text
|
||||||
|
snackbar.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyMessage = () => {
|
||||||
|
copy(props.message.message)
|
||||||
|
showSnackbar('Copied!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const editMessage = () => {
|
||||||
|
props.usePrompt(props.message.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteMessage = async () => {
|
||||||
|
const { data, error } = await useAuthFetch(`/api/chat/messages/${props.message.id}/`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
if (!error.value) {
|
||||||
|
props.deleteMessage(props.messageIndex)
|
||||||
|
showSnackbar('Deleted!')
|
||||||
|
}
|
||||||
|
showSnackbar('Delete failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-menu
|
||||||
|
>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
v-bind="props"
|
||||||
|
icon
|
||||||
|
variant="text"
|
||||||
|
class="mx-1"
|
||||||
|
>
|
||||||
|
<v-icon icon="more_horiz"></v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
@click="copyMessage()"
|
||||||
|
:title="$t('copy')"
|
||||||
|
prepend-icon="content_copy"
|
||||||
|
>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item
|
||||||
|
@click="editMessage()"
|
||||||
|
:title="$t('edit')"
|
||||||
|
prepend-icon="edit"
|
||||||
|
>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item
|
||||||
|
@click="deleteMessage()"
|
||||||
|
:title="$t('delete')"
|
||||||
|
prepend-icon="delete"
|
||||||
|
>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
|
||||||
|
<v-snackbar
|
||||||
|
v-model="snackbar"
|
||||||
|
location="top"
|
||||||
|
timeout="2000"
|
||||||
|
>
|
||||||
|
{{ snackbarText }}
|
||||||
|
</v-snackbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
201
components/ModelParameters.client.vue
Normal file
201
components/ModelParameters.client.vue
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
const dialog = ref(false)
|
||||||
|
const currentModel = useCurrentModel()
|
||||||
|
const availableModels = [
|
||||||
|
'gpt-3.5-turbo',
|
||||||
|
'gpt-4'
|
||||||
|
]
|
||||||
|
const currentModelDefault = ref(MODELS[currentModel.value.name])
|
||||||
|
|
||||||
|
onNuxtReady(() => {
|
||||||
|
currentModel.value = getCurrentModel()
|
||||||
|
watch(currentModel, (newVal, oldVal) => {
|
||||||
|
currentModelDefault.value = MODELS[newVal.name]
|
||||||
|
saveCurrentModel(newVal)
|
||||||
|
}, { deep: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-dialog
|
||||||
|
v-model="dialog"
|
||||||
|
persistent
|
||||||
|
>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-list-item
|
||||||
|
v-bind="props"
|
||||||
|
rounded="xl"
|
||||||
|
prepend-icon="tune"
|
||||||
|
:title="$t('modelParameters')"
|
||||||
|
></v-list-item>
|
||||||
|
</template>
|
||||||
|
<v-card>
|
||||||
|
<v-toolbar
|
||||||
|
density="compact"
|
||||||
|
>
|
||||||
|
<v-toolbar-title>{{ $t('modelParameters') }}</v-toolbar-title>
|
||||||
|
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-btn icon="close" @click="dialog = false"></v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
<v-card-text>
|
||||||
|
<v-select
|
||||||
|
v-model="currentModel.name"
|
||||||
|
:label="$t('model')"
|
||||||
|
:items="availableModels"
|
||||||
|
variant="underlined"
|
||||||
|
></v-select>
|
||||||
|
|
||||||
|
<v-row
|
||||||
|
no-gutters
|
||||||
|
>
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="d-flex justify-space-between align-center">
|
||||||
|
<v-list-subheader>{{ $t('temperature') }}</v-list-subheader>
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="currentModel.temperature"
|
||||||
|
hide-details
|
||||||
|
single-line
|
||||||
|
density="compact"
|
||||||
|
type="number"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
style="width: 100px"
|
||||||
|
class="flex-grow-0"
|
||||||
|
></v-text-field>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-slider
|
||||||
|
v-model="currentModel.temperature"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
hide-details
|
||||||
|
>
|
||||||
|
</v-slider>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row
|
||||||
|
no-gutters
|
||||||
|
>
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="d-flex justify-space-between align-center">
|
||||||
|
<v-list-subheader>{{ $t('maxTokens') }}</v-list-subheader>
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="currentModel.max_tokens"
|
||||||
|
hide-details
|
||||||
|
single-line
|
||||||
|
density="compact"
|
||||||
|
type="number"
|
||||||
|
:max="currentModelDefault.total_tokens"
|
||||||
|
step="1"
|
||||||
|
style="width: 100px"
|
||||||
|
class="flex-grow-0"
|
||||||
|
></v-text-field>
|
||||||
|
</div>
|
||||||
|
<div class="text-caption">
|
||||||
|
{{ $t('maxTokenTips1') }} <b>{{ currentModelDefault.total_tokens }}</b> {{ $t('maxTokenTips2') }}
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-slider
|
||||||
|
v-model="currentModel.max_tokens"
|
||||||
|
:max="currentModelDefault.total_tokens"
|
||||||
|
:step="1"
|
||||||
|
hide-details
|
||||||
|
>
|
||||||
|
</v-slider>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row
|
||||||
|
no-gutters
|
||||||
|
>
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="d-flex justify-space-between align-center">
|
||||||
|
<v-list-subheader>{{ $t('topP') }}</v-list-subheader>
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="currentModel.top_p"
|
||||||
|
hide-details
|
||||||
|
single-line
|
||||||
|
density="compact"
|
||||||
|
type="number"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
style="width: 100px"
|
||||||
|
class="flex-grow-0"
|
||||||
|
></v-text-field>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-slider
|
||||||
|
v-model="currentModel.top_p"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
hide-details
|
||||||
|
>
|
||||||
|
</v-slider>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row no-gutters>
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="d-flex justify-space-between align-center">
|
||||||
|
<v-list-subheader>{{ $t('frequencyPenalty') }}</v-list-subheader>
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="currentModel.frequency_penalty"
|
||||||
|
hide-details
|
||||||
|
single-line
|
||||||
|
density="compact"
|
||||||
|
type="number"
|
||||||
|
max="2"
|
||||||
|
step="0.01"
|
||||||
|
style="width: 100px"
|
||||||
|
class="flex-grow-0"
|
||||||
|
></v-text-field>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-slider
|
||||||
|
v-model="currentModel.frequency_penalty"
|
||||||
|
:max="2"
|
||||||
|
:step="0.01"
|
||||||
|
hide-details
|
||||||
|
></v-slider>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row no-gutters>
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="d-flex justify-space-between align-center">
|
||||||
|
<v-list-subheader>{{ $t('presencePenalty') }}</v-list-subheader>
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="currentModel.presence_penalty"
|
||||||
|
hide-details
|
||||||
|
single-line
|
||||||
|
density="compact"
|
||||||
|
type="number"
|
||||||
|
max="2"
|
||||||
|
step="0.01"
|
||||||
|
style="width: 100px"
|
||||||
|
class="flex-grow-0"
|
||||||
|
></v-text-field>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-slider
|
||||||
|
v-model="currentModel.presence_penalty"
|
||||||
|
:max="2"
|
||||||
|
:step="0.01"
|
||||||
|
hide-details
|
||||||
|
></v-slider>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,26 +1,92 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { marked } from "marked"
|
|
||||||
import hljs from "highlight.js"
|
import hljs from "highlight.js"
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
import copy from 'copy-to-clipboard'
|
||||||
|
|
||||||
marked.setOptions({
|
|
||||||
highlight: function (code, lang) {
|
const md = new MarkdownIt({
|
||||||
|
linkify: true,
|
||||||
|
highlight(code, lang) {
|
||||||
const language = hljs.getLanguage(lang) ? lang : 'plaintext'
|
const language = hljs.getLanguage(lang) ? lang : 'plaintext'
|
||||||
return hljs.highlight(code, { language }).value
|
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>`
|
||||||
},
|
},
|
||||||
langPrefix: 'hljs language-', // highlight.js css class prefix
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const props = defineProps(['content'])
|
const props = defineProps({
|
||||||
const contentHtml = computed(() => {
|
message: {
|
||||||
return props.content ? marked(props.content) : ''
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const contentHtml = ref('')
|
||||||
|
|
||||||
|
const contentElm = ref(null)
|
||||||
|
|
||||||
|
watchEffect(async () => {
|
||||||
|
contentHtml.value = props.message.message ? md.render(props.message.message) : ''
|
||||||
|
await nextTick()
|
||||||
|
bindCopyCodeToButtons()
|
||||||
|
})
|
||||||
|
|
||||||
|
const bindCopyCodeToButtons = () => {
|
||||||
|
if (!contentElm.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
contentElm.value.querySelectorAll('.hljs-code-container').forEach((codeContainer) => {
|
||||||
|
const copyButton = codeContainer.querySelector('.hljs-copy-button');
|
||||||
|
const codeBody = codeContainer.querySelector('code');
|
||||||
|
copyButton.onclick = function () {
|
||||||
|
copy(codeBody.textContent ?? '');
|
||||||
|
|
||||||
|
copyButton.innerHTML = "Copied!";
|
||||||
|
copyButton.dataset.copied = 'true';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
copyButton.innerHTML = "Copy";
|
||||||
|
copyButton.dataset.copied = 'false';
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
bindCopyCodeToButtons()
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<v-card
|
||||||
v-html="contentHtml"
|
:color="message.is_bot ? '' : 'primary'"
|
||||||
class="text-body-1 text-justify"
|
rounded="lg"
|
||||||
></div>
|
elevation="2"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="contentElm"
|
||||||
|
v-html="contentHtml"
|
||||||
|
class="chat-msg-content pa-3"
|
||||||
|
></div>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
</template>
|
<style>
|
||||||
|
.chat-msg-content ol, .chat-msg-content ul {
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
.hljs-code-container {
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.hljs-copy-button{
|
||||||
|
width:2rem;height:2rem;text-indent:-9999px;color:#fff;
|
||||||
|
border-radius:.25rem;border:1px solid #ffffff22;
|
||||||
|
background-image:url('data:image/svg+xml;utf-8,<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M6 5C5.73478 5 5.48043 5.10536 5.29289 5.29289C5.10536 5.48043 5 5.73478 5 6V20C5 20.2652 5.10536 20.5196 5.29289 20.7071C5.48043 20.8946 5.73478 21 6 21H18C18.2652 21 18.5196 20.8946 18.7071 20.7071C18.8946 20.5196 19 20.2652 19 20V6C19 5.73478 18.8946 5.48043 18.7071 5.29289C18.5196 5.10536 18.2652 5 18 5H16C15.4477 5 15 4.55228 15 4C15 3.44772 15.4477 3 16 3H18C18.7956 3 19.5587 3.31607 20.1213 3.87868C20.6839 4.44129 21 5.20435 21 6V20C21 20.7957 20.6839 21.5587 20.1213 22.1213C19.5587 22.6839 18.7957 23 18 23H6C5.20435 23 4.44129 22.6839 3.87868 22.1213C3.31607 21.5587 3 20.7957 3 20V6C3 5.20435 3.31607 4.44129 3.87868 3.87868C4.44129 3.31607 5.20435 3 6 3H8C8.55228 3 9 3.44772 9 4C9 4.55228 8.55228 5 8 5H6Z" fill="white"/><path fill-rule="evenodd" clip-rule="evenodd" d="M7 3C7 1.89543 7.89543 1 9 1H15C16.1046 1 17 1.89543 17 3V5C17 6.10457 16.1046 7 15 7H9C7.89543 7 7 6.10457 7 5V3ZM15 3H9V5H15V3Z" fill="white"/></svg>');
|
||||||
|
background-repeat:no-repeat;background-position:center;
|
||||||
|
transition:background-color 200ms ease,transform 200ms ease-out
|
||||||
|
}
|
||||||
|
.hljs-copy-button:hover{border-color:#ffffff44}
|
||||||
|
.hljs-copy-button:active{border-color:#ffffff66}
|
||||||
|
.hljs-copy-button[data-copied="true"]{text-indent:0;width:auto;background-image:none}
|
||||||
|
@media(prefers-reduced-motion){.hljs-copy-button{transition:none}}
|
||||||
|
</style>
|
||||||
@@ -1,76 +1,97 @@
|
|||||||
<template>
|
<script setup>
|
||||||
<v-textarea
|
|
||||||
v-model="message"
|
|
||||||
:label="$t('writeAMessage')"
|
|
||||||
:placeholder="$t('writeAMessage') + '...'"
|
|
||||||
rows="1"
|
|
||||||
:auto-grow="autoGrow"
|
|
||||||
:disabled="disabled"
|
|
||||||
:loading="loading"
|
|
||||||
:hint="hint"
|
|
||||||
:hide-details="loading"
|
|
||||||
append-inner-icon="send"
|
|
||||||
@keyup.enter.exact="enterOnly"
|
|
||||||
@click:appendInner="clickSendBtn"
|
|
||||||
></v-textarea>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { isMobile } from 'is-mobile'
|
import { isMobile } from 'is-mobile'
|
||||||
export default {
|
const { $i18n } = useNuxtApp()
|
||||||
name: "MsgEditor",
|
|
||||||
props: {
|
const props = defineProps({
|
||||||
sendMessage: Function,
|
sendMessage: {
|
||||||
disabled: Boolean,
|
type: Function,
|
||||||
loading: Boolean,
|
required: true
|
||||||
},
|
},
|
||||||
data() {
|
disabled: {
|
||||||
return {
|
type: Boolean,
|
||||||
message: "",
|
default: false
|
||||||
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 = lines;
|
|
||||||
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 = ""
|
|
||||||
},
|
|
||||||
clickSendBtn () {
|
|
||||||
this.send()
|
|
||||||
},
|
|
||||||
enterOnly () {
|
|
||||||
if (!isMobile()) {
|
|
||||||
this.send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
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 usePrompt = (prompt) => {
|
||||||
|
message.value = prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickSendBtn = () => {
|
||||||
|
send()
|
||||||
|
}
|
||||||
|
|
||||||
|
const enterOnly = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!isMobile()) {
|
||||||
|
send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
usePrompt
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<template>
|
||||||
</style>
|
<div
|
||||||
|
class="flex-grow-1 d-flex align-center justify-space-between"
|
||||||
|
>
|
||||||
|
<v-textarea
|
||||||
|
v-model="message"
|
||||||
|
:label="$t('writeAMessage')"
|
||||||
|
:placeholder="hint"
|
||||||
|
:rows="rows"
|
||||||
|
max-rows="8"
|
||||||
|
:auto-grow="autoGrow"
|
||||||
|
:disabled="disabled"
|
||||||
|
:loading="loading"
|
||||||
|
:hide-details="true"
|
||||||
|
clearable
|
||||||
|
variant="outlined"
|
||||||
|
@keydown.enter.exact="enterOnly"
|
||||||
|
></v-textarea>
|
||||||
|
<v-btn
|
||||||
|
:disabled="loading"
|
||||||
|
icon="send"
|
||||||
|
title="Send"
|
||||||
|
class="ml-3"
|
||||||
|
@click="clickSendBtn"
|
||||||
|
></v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
346
components/NavigationDrawer.vue
Normal file
346
components/NavigationDrawer.vue
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
import {useDrawer} from "../composables/states";
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const { $i18n } = 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 settings = useSettings()
|
||||||
|
const showApiKeySetting = computed(() => {
|
||||||
|
return settings.value && settings.value.open_api_key_setting && settings.value.open_api_key_setting === 'True'
|
||||||
|
})
|
||||||
|
|
||||||
|
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="showApiKeySetting"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
||||||
261
components/Prompt.vue
Normal file
261
components/Prompt.vue
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
<script setup>
|
||||||
|
const menu = ref(false)
|
||||||
|
const prompts = ref([])
|
||||||
|
const editingPrompt = ref(null)
|
||||||
|
const newTitlePrompt = ref(null)
|
||||||
|
const newPrompt = ref('')
|
||||||
|
const submittingNewPrompt = ref(false)
|
||||||
|
const promptInputErrorMessage = ref('')
|
||||||
|
const loadingPrompts = ref(false)
|
||||||
|
const deletingPromptIndex = ref(null)
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
usePrompt: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const addPrompt = async () => {
|
||||||
|
if (!newPrompt.value) {
|
||||||
|
promptInputErrorMessage.value = 'Please enter a prompt'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submittingNewPrompt.value = true
|
||||||
|
const { data, error } = await useAuthFetch('/api/chat/prompts/', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: newTitlePrompt.value,
|
||||||
|
prompt: newPrompt.value
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!error.value) {
|
||||||
|
prompts.value.push(data.value)
|
||||||
|
newTitlePrompt.value = null
|
||||||
|
newPrompt.value = ''
|
||||||
|
}
|
||||||
|
submittingNewPrompt.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const editPrompt = (index) => {
|
||||||
|
editingPrompt.value = Object.assign({}, prompts.value[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePrompt = async (index) => {
|
||||||
|
editingPrompt.value.updating = true
|
||||||
|
const { data, error } = await useAuthFetch(`/api/chat/prompts/${editingPrompt.value.id}/`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: editingPrompt.value.title,
|
||||||
|
prompt: editingPrompt.value.prompt
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!error.value) {
|
||||||
|
prompts.value[index] = editingPrompt.value
|
||||||
|
}
|
||||||
|
editingPrompt.value.updating = false
|
||||||
|
editingPrompt.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelEditPrompt = () => {
|
||||||
|
editingPrompt.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletePrompt = async (index) => {
|
||||||
|
deletingPromptIndex.value = index
|
||||||
|
const { data, error } = await useAuthFetch(`/api/chat/prompts/${prompts.value[index].id}/`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
deletingPromptIndex.value = null
|
||||||
|
if (!error.value) {
|
||||||
|
prompts.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadPrompts = async () => {
|
||||||
|
loadingPrompts.value = true
|
||||||
|
const { data, error } = await useAuthFetch('/api/chat/prompts/')
|
||||||
|
if (!error.value) {
|
||||||
|
prompts.value = data.value
|
||||||
|
}
|
||||||
|
loadingPrompts.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectPrompt = (prompt) => {
|
||||||
|
props.usePrompt(prompt.prompt)
|
||||||
|
menu.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onNuxtReady( () => {
|
||||||
|
loadPrompts()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-menu
|
||||||
|
v-model="menu"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
v-bind="props"
|
||||||
|
icon
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
icon="speaker_notes"
|
||||||
|
></v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-container>
|
||||||
|
<v-card
|
||||||
|
min-width="300"
|
||||||
|
max-width="500"
|
||||||
|
>
|
||||||
|
<v-card-title>
|
||||||
|
<span class="headline">{{ $t('frequentlyPrompts') }}</span>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-list>
|
||||||
|
<v-list-item v-show="loadingPrompts">
|
||||||
|
<v-list-item-title class="d-flex justify-center">
|
||||||
|
<v-progress-circular indeterminate></v-progress-circular>
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<template
|
||||||
|
v-for="(prompt, idx) in prompts"
|
||||||
|
:key="prompt.id"
|
||||||
|
>
|
||||||
|
<v-list-item
|
||||||
|
active-color="primary"
|
||||||
|
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
|
||||||
|
rows="2"
|
||||||
|
v-model="editingPrompt.prompt"
|
||||||
|
:loading="editingPrompt.updating"
|
||||||
|
variant="underlined"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
>
|
||||||
|
</v-textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<v-btn
|
||||||
|
icon="done"
|
||||||
|
variant="text"
|
||||||
|
:loading="editingPrompt.updating"
|
||||||
|
@click="updatePrompt(idx)"
|
||||||
|
>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon="close"
|
||||||
|
variant="text"
|
||||||
|
@click="cancelEditPrompt()"
|
||||||
|
>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item
|
||||||
|
v-if="!editingPrompt || editingPrompt.id !== prompt.id"
|
||||||
|
rounded="xl"
|
||||||
|
active-color="primary"
|
||||||
|
@click="selectPrompt(prompt)"
|
||||||
|
>
|
||||||
|
<v-list-item-title>{{ prompt.title ? prompt.title : prompt.prompt }}</v-list-item-title>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-btn
|
||||||
|
icon="edit"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="editPrompt(idx)"
|
||||||
|
>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon="delete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
:loading="deletingPromptIndex === idx"
|
||||||
|
@click="deletePrompt(idx)"
|
||||||
|
>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</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
|
||||||
|
active-color="primary"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="pt-3"
|
||||||
|
>
|
||||||
|
<v-textarea
|
||||||
|
rows="2"
|
||||||
|
v-model="newPrompt"
|
||||||
|
:label="$t('addNewPrompt')"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
:error-messages="promptInputErrorMessage"
|
||||||
|
@update:modelValue="promptInputErrorMessage = ''"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
</v-textarea>
|
||||||
|
</div>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
block
|
||||||
|
:loading="submittingNewPrompt"
|
||||||
|
@click="addPrompt()"
|
||||||
|
>
|
||||||
|
<v-icon icon="add"></v-icon>
|
||||||
|
{{ $t('addPrompt') }}
|
||||||
|
</v-btn>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</v-menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -15,26 +15,23 @@
|
|||||||
</template>
|
</template>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-toolbar
|
<v-toolbar
|
||||||
dark
|
|
||||||
color="primary"
|
|
||||||
>
|
>
|
||||||
<v-btn
|
<v-btn
|
||||||
icon
|
icon
|
||||||
dark
|
|
||||||
@click="dialog = false"
|
@click="dialog = false"
|
||||||
>
|
>
|
||||||
<v-icon>close</v-icon>
|
<v-icon icon="close"></v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-toolbar-title>{{ $t('language') }}</v-toolbar-title>
|
<v-toolbar-title>{{ $t('language') }}</v-toolbar-title>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<!-- <v-toolbar-items>-->
|
<v-toolbar-items>
|
||||||
<!-- <v-btn-->
|
<v-btn
|
||||||
<!-- variant="text"-->
|
variant="text"
|
||||||
<!-- @click="dialog = false"-->
|
@click="dialog = false"
|
||||||
<!-- >-->
|
>
|
||||||
<!-- Save-->
|
Save
|
||||||
<!-- </v-btn>-->
|
</v-btn>
|
||||||
<!-- </v-toolbar-items>-->
|
</v-toolbar-items>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
<v-list
|
<v-list
|
||||||
>
|
>
|
||||||
|
|||||||
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,10 +1,14 @@
|
|||||||
|
|
||||||
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 useSettings = () => useState('settings', () => {})
|
||||||
|
|
||||||
|
export const useUser = () => useState('user', () => null)
|
||||||
|
|
||||||
|
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
|
|
||||||
}
|
|
||||||
BIN
demos/bmc_qr.png
Normal file
BIN
demos/bmc_qr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
BIN
demos/demo.gif
Normal file
BIN
demos/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
BIN
demos/demo.mp4
Normal file
BIN
demos/demo.mp4
Normal file
Binary file not shown.
BIN
demos/demo.png
BIN
demos/demo.png
Binary file not shown.
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 47 KiB |
@@ -1,6 +1,28 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
read -p "Please enter a resolved domain name: " domain
|
read -p "Please enter a domain name or external IP address [default: localhost]: " APP_DOMAIN
|
||||||
|
|
||||||
|
if [ -z "$APP_DOMAIN" ]; then
|
||||||
|
APP_DOMAIN="localhost"
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -p "Please set a port for the frontend server [default: 80]: " CLIENT_PORT
|
||||||
|
|
||||||
|
if [ -z "$CLIENT_PORT" ]; then
|
||||||
|
CLIENT_PORT="80"
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -p "Please set a port for the backend server [default: 9000]: " SERVER_PORT
|
||||||
|
|
||||||
|
if [ -z "$SERVER_PORT" ]; then
|
||||||
|
SERVER_PORT="9000"
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -p "Please set a port for the backend WSGI server [default: 8000]: " WSGI_PORT
|
||||||
|
|
||||||
|
if [ -z "$WSGI_PORT" ]; then
|
||||||
|
WSGI_PORT="8000"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ $(which docker) ]]; then
|
if [[ $(which docker) ]]; then
|
||||||
echo "Docker is already installed"
|
echo "Docker is already installed"
|
||||||
@@ -43,6 +65,6 @@ sudo curl -L "https://raw.githubusercontent.com/WongSaang/chatgpt-ui/main/docker
|
|||||||
|
|
||||||
echo "Starting services..."
|
echo "Starting services..."
|
||||||
|
|
||||||
sudo APP_DOMAIN="${domain}:9000" docker-compose up -d
|
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
|
||||||
|
|
||||||
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,44 +1,56 @@
|
|||||||
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' # 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:
|
depends_on:
|
||||||
- backend-web-server
|
- backend-web-server
|
||||||
ports:
|
ports:
|
||||||
- '80:80'
|
- '${CLIENT_PORT:-80}:80'
|
||||||
networks:
|
networks:
|
||||||
- chatgpt_ui_network
|
- chatgpt_ui_network
|
||||||
|
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:
|
||||||
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000}
|
- 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.
|
# - 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_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
|
||||||
|
- 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
|
# If you want to use the email verification function, you need to configure the following parameters
|
||||||
# - EMAIL_HOST=SMTP server address
|
# - EMAIL_HOST=SMTP server address
|
||||||
# - EMAIL_PORT=SMTP server port
|
# - EMAIL_PORT=SMTP server port
|
||||||
# - EMAIL_HOST_USER=
|
# - EMAIL_HOST_USER=
|
||||||
# - EMAIL_HOST_PASSWORD=
|
# - EMAIL_HOST_PASSWORD=
|
||||||
# - EMAIL_USE_TLS=True
|
# - EMAIL_USE_TLS=True
|
||||||
|
# - EMAIL_FROM=no-reply@example.com #Default sender email address
|
||||||
ports:
|
ports:
|
||||||
- '8000:8000'
|
- '${WSGI_PORT:-8000}:8000'
|
||||||
networks:
|
networks:
|
||||||
- chatgpt_ui_network
|
- chatgpt_ui_network
|
||||||
|
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
|
||||||
ports:
|
ports:
|
||||||
- '9000:80'
|
- '${SERVER_PORT:-9000}:80'
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend-wsgi-server
|
- backend-wsgi-server
|
||||||
networks:
|
networks:
|
||||||
- chatgpt_ui_network
|
- chatgpt_ui_network
|
||||||
|
restart: always
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
chatgpt_ui_network:
|
chatgpt_ui_network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
206
docs/zh/README.md
Normal file
206
docs/zh/README.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<div align="center">
|
||||||
|
<h1>ChatGPT UI</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
[English](../../README.md) | [中文](./docs/zh/README.md)
|
||||||
|
|
||||||
|
用户指南: [https://wongsaang.github.io/chatgpt-ui-docs/zh/](https://wongsaang.github.io/chatgpt-ui-docs/zh/)
|
||||||
|
|
||||||
|
ChatGPT Web 客户端,支持多用户,支持 Mysql、PostgreSQL 等多种数据库连接进行数据持久化存储,支持多语言。提供 Docker 镜像和快速部署脚本。
|
||||||
|
|
||||||
|
本项目的服务端:[https://github.com/WongSaang/chatgpt-ui-server](https://github.com/WongSaang/chatgpt-ui-server)
|
||||||
|
|
||||||
|
https://user-images.githubusercontent.com/46235412/227156264-ca17ab17-999b-414f-ab06-3f75b5235bfe.mp4
|
||||||
|
|
||||||
|
|
||||||
|
## 📢 更新
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary><strong>2023-04-06</strong></summary>
|
||||||
|
客户端改成服务端渲染(SSR)的方式部署,现在可以使用环境变量了,可用环境变量请看下方 docker-compose 配置。提升了首屏加载速度,减少白屏时间。
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary><strong>2023-03-27</strong></summary>
|
||||||
|
🚀 支持 gpt-4 模型。你可以在前端的“模型参数”中选择模型,gpt-4 模型需要通过 openai 的白名单才能使用。
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
在管理后台增加 `open_registration` 设置项,用于控制是否开放用户注册。你可以登录管理后台,在 `Chat->Setting` 中看到这个设置项,默认是 `True` (允许用户注册),如果不需要,请改成 `False`。
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<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 上验证过。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -Ls https://raw.githubusercontent.com/WongSaang/chatgpt-ui/main/deployment.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
> 如果您拥有一个域名,可以使用 DNS 解析将其指向服务器的 IP 地址。当然,直接使用服务器的 IP 地址也是可以的。
|
||||||
|
> 在脚本执行期间,会提示您输入域名。如果您没有域名,可以直接输入服务器的 IP 地址。
|
||||||
|
|
||||||
|
### 部署完成之后
|
||||||
|
|
||||||
|
访问 `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 名称
|
||||||
|
# - 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
```
|
||||||
@@ -10,17 +10,50 @@
|
|||||||
"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",
|
||||||
"followSystem": "Follow system",
|
"followSystem": "Follow system",
|
||||||
"themeMode": "Theme Mode",
|
"themeMode": "Theme Mode",
|
||||||
"feedback": "Feedback",
|
"feedback": "Feedback",
|
||||||
|
"newConversation": "New conversation",
|
||||||
|
"defaultConversationTitle": "Unnamed",
|
||||||
"clearConversations": "Clear conversations",
|
"clearConversations": "Clear conversations",
|
||||||
|
"modelParameters": "Model Parameters",
|
||||||
|
"model": "Model",
|
||||||
|
"temperature": "Temperature",
|
||||||
|
"topP": "Top P",
|
||||||
|
"frequencyPenalty": "Frequency Penalty",
|
||||||
|
"presencePenalty": "Presence Penalty",
|
||||||
|
"maxTokens": "Max Tokens",
|
||||||
"roles": {
|
"roles": {
|
||||||
"me": "Me",
|
"me": "Me",
|
||||||
"ai": "AI"
|
"ai": "AI"
|
||||||
},
|
},
|
||||||
|
"edit": "Edit",
|
||||||
|
"copy": "Copy",
|
||||||
|
"copied": "Copied",
|
||||||
|
"delete": "Delete",
|
||||||
|
"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",
|
||||||
|
"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.",
|
||||||
|
|||||||
79
lang/ru-RU.json
Normal file
79
lang/ru-RU.json
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"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 года"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,17 +10,50 @@
|
|||||||
"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": "暗色模式",
|
||||||
"followSystem": "跟随系统",
|
"followSystem": "跟随系统",
|
||||||
"themeMode": "主题模式",
|
"themeMode": "主题模式",
|
||||||
"feedback": "反馈",
|
"feedback": "反馈",
|
||||||
"clearConversations": "清除会话",
|
"newConversation": "新的对话",
|
||||||
|
"defaultConversationTitle": "未命名",
|
||||||
|
"clearConversations": "清除对话",
|
||||||
|
"modelParameters": "模型参数",
|
||||||
|
"model": "模型",
|
||||||
|
"temperature": "Temperature",
|
||||||
|
"topP": "Top P",
|
||||||
|
"frequencyPenalty": "Frequency Penalty",
|
||||||
|
"presencePenalty": "Presence Penalty",
|
||||||
|
"maxTokens": "Max Tokens",
|
||||||
"roles": {
|
"roles": {
|
||||||
"me": "我",
|
"me": "我",
|
||||||
"ai": "AI"
|
"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": "当前模型的最大上下文长度为",
|
||||||
|
"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,296 +1,8 @@
|
|||||||
<script setup>
|
|
||||||
import {useConversions} from "../composables/states";
|
|
||||||
import {getConversions} from "../utils/helper";
|
|
||||||
|
|
||||||
const { $i18n } = 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 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) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
onNuxtReady(async () => {
|
|
||||||
loadConversations()
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-app
|
<v-app
|
||||||
:theme="$colorMode.value"
|
:theme="$colorMode.value"
|
||||||
>
|
>
|
||||||
<v-navigation-drawer
|
<NavigationDrawer />
|
||||||
v-model="drawer"
|
<slot />
|
||||||
>
|
|
||||||
<div class="px-2 py-2">
|
|
||||||
<v-list>
|
|
||||||
<v-list-item>
|
|
||||||
<v-btn
|
|
||||||
block
|
|
||||||
variant="outlined"
|
|
||||||
prepend-icon="add"
|
|
||||||
@click="createNewConversion()"
|
|
||||||
class="text-none"
|
|
||||||
>
|
|
||||||
New conversation
|
|
||||||
</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"
|
|
||||||
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
|
|
||||||
@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
|
|
||||||
active-color="primary"
|
|
||||||
@click="openConversationMessages(conversation)"
|
|
||||||
v-bind="props"
|
|
||||||
>
|
|
||||||
<v-list-item-title>{{ conversation.topic }}</v-list-item-title>
|
|
||||||
<v-list-item-action v-show="isHovering">
|
|
||||||
<v-btn
|
|
||||||
icon="edit"
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
@click="editConversation(cIdx)"
|
|
||||||
>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon="delete"
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
:loading="deletingConversationIndex === cIdx"
|
|
||||||
@click="deleteConversation(cIdx)"
|
|
||||||
>
|
|
||||||
</v-btn>
|
|
||||||
</v-list-item-action>
|
|
||||||
</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
|
|
||||||
width="auto"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</v-navigation-drawer>
|
|
||||||
|
|
||||||
<v-app-bar
|
|
||||||
class="d-lg-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-menu
|
|
||||||
>
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<v-btn
|
|
||||||
v-bind="props"
|
|
||||||
icon="help_outline"
|
|
||||||
title="Feedback"
|
|
||||||
></v-btn>
|
|
||||||
</template>
|
|
||||||
<v-list
|
|
||||||
>
|
|
||||||
<v-list-item
|
|
||||||
@click="feedback"
|
|
||||||
>
|
|
||||||
<v-list-item-title>{{ $t('feedback') }}</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
</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,
|
||||||
@@ -11,7 +10,10 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
appName: appName
|
appName: appName,
|
||||||
|
typewriter: false,
|
||||||
|
typewriterDelay: 50,
|
||||||
|
customApiKey: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
@@ -23,9 +25,20 @@ export default defineNuxtConfig({
|
|||||||
'highlight.js/styles/panda-syntax-dark.css',
|
'highlight.js/styles/panda-syntax-dark.css',
|
||||||
],
|
],
|
||||||
modules: [
|
modules: [
|
||||||
|
'@kevinmarrec/nuxt-pwa',
|
||||||
'@nuxtjs/color-mode',
|
'@nuxtjs/color-mode',
|
||||||
'@nuxtjs/i18n'
|
'@nuxtjs/i18n'
|
||||||
],
|
],
|
||||||
|
pwa: {
|
||||||
|
manifest: {
|
||||||
|
name: appName,
|
||||||
|
short_name: appName,
|
||||||
|
description: 'A ChatGPT web Client'
|
||||||
|
},
|
||||||
|
workbox: {
|
||||||
|
enabled: process.env.DEBUT_PWA === 'true',
|
||||||
|
}
|
||||||
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
strategy: 'no_prefix',
|
strategy: 'no_prefix',
|
||||||
locales: [
|
locales: [
|
||||||
@@ -40,6 +53,12 @@ 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',
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
lazy: true,
|
lazy: true,
|
||||||
@@ -48,15 +67,5 @@ export default defineNuxtConfig({
|
|||||||
vueI18n: {
|
vueI18n: {
|
||||||
fallbackLocale: 'en',
|
fallbackLocale: 'en',
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
nitro: {
|
|
||||||
devProxy: {
|
|
||||||
"/api": {
|
|
||||||
target: "http://localhost:8000/api",
|
|
||||||
prependPath: true,
|
|
||||||
changeOrigin: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,16 +8,19 @@
|
|||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@kevinmarrec/nuxt-pwa": "^0.17.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",
|
||||||
"material-design-icons-iconfont": "^6.7.0",
|
"material-design-icons-iconfont": "^6.7.0",
|
||||||
"nuxt": "^3.2.0"
|
"nuxt": "^3.3.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@microsoft/fetch-event-source": "^2.0.1",
|
"@microsoft/fetch-event-source": "^2.0.1",
|
||||||
|
"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",
|
||||||
"marked": "^4.2.12",
|
"markdown-it": "^13.0.1",
|
||||||
"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
|
||||||
@@ -45,24 +46,32 @@ onNuxtReady(() => {
|
|||||||
elevation="0"
|
elevation="0"
|
||||||
>
|
>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h2 class="text-h4">Verify your email</h2>
|
<div v-if="route.query.email_verification_required && route.query.email_verification_required === 'none'">
|
||||||
<p class="text-body-2 mt-5">
|
<h2 class="text-h4">Your registration is successful</h2>
|
||||||
We've sent a verification email to <strong>{{ $auth.user.email }}</strong>. <br>
|
<p class="mt-5">
|
||||||
Please check your inbox and click the link to verify your email address.
|
You can now <NuxtLink to="/account/signin">login</NuxtLink> to your account.
|
||||||
</p>
|
</p>
|
||||||
<p v-if="errorMsg"
|
</div>
|
||||||
class="text-red"
|
<div v-else>
|
||||||
>{{ errorMsg }}</p>
|
<h2 class="text-h4">Verify your email</h2>
|
||||||
<v-btn
|
<p class="mt-5">
|
||||||
variant="text"
|
We've sent a verification email to <strong>{{ user.email }}</strong>. <br>
|
||||||
class="mt-5"
|
Please check your inbox and click the link to verify your email address.
|
||||||
color="primary"
|
</p>
|
||||||
:loading="sending"
|
<p v-if="errorMsg"
|
||||||
@click="resendEmail"
|
class="text-red"
|
||||||
:disabled="resent"
|
>{{ errorMsg }}</p>
|
||||||
>
|
<v-btn
|
||||||
{{ resent ? 'Resent' : 'Resend email'}}
|
variant="text"
|
||||||
</v-btn>
|
class="mt-5"
|
||||||
|
color="primary"
|
||||||
|
:loading="sending"
|
||||||
|
@click="resendEmail"
|
||||||
|
:disabled="resent"
|
||||||
|
>
|
||||||
|
{{ resent ? 'Resent' : 'Resend email'}}
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|||||||
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>
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
:rules="formRules.username"
|
:rules="formRules.username"
|
||||||
label="User name"
|
label="User name"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
|
clearable
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="formData.password"
|
v-model="formData.password"
|
||||||
@@ -29,6 +30,10 @@
|
|||||||
label="Password"
|
label="Password"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
|
clearable
|
||||||
|
:type="passwordInputType"
|
||||||
|
:append-inner-icon="passwordInputType === 'password' ? 'visibility' : 'visibility_off'"
|
||||||
|
@click:append-inner="passwordInputType = passwordInputType === 'password' ? 'text' : 'password'"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
</v-form>
|
</v-form>
|
||||||
@@ -62,6 +67,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import {useUser} from "~/composables/states";
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'vuetify-app'
|
layout: 'vuetify-app'
|
||||||
})
|
})
|
||||||
@@ -77,12 +84,11 @@ const formRules = ref({
|
|||||||
v => !!v || 'Password is required'
|
v => !!v || '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 submit = async () => {
|
const submit = async () => {
|
||||||
errorMsg.value = null
|
errorMsg.value = null
|
||||||
@@ -93,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) {
|
||||||
@@ -102,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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ definePageMeta({
|
|||||||
layout: 'vuetify-app'
|
layout: 'vuetify-app'
|
||||||
})
|
})
|
||||||
|
|
||||||
const { $auth } = useNuxtApp()
|
|
||||||
|
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
@@ -67,11 +65,15 @@ const submit = async () => {
|
|||||||
errorMsg.value = error.value.data.non_field_errors[0]
|
errorMsg.value = error.value.data.non_field_errors[0]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errorMsg.value = 'Something went wrong. Please try again.'
|
if (error.value.data.detail) {
|
||||||
|
errorMsg.value = error.value.data.detail
|
||||||
|
} else {
|
||||||
|
errorMsg.value = 'Something went wrong. Please try again.'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$auth.setUser(data.value.user)
|
setUser(data.value.user)
|
||||||
navigateTo('/account/onboarding')
|
navigateTo('/account/onboarding?email_verification_required='+data.value.email_verification_required)
|
||||||
}
|
}
|
||||||
|
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
@@ -79,7 +81,7 @@ const submit = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleFieldUpdate = (field) => {
|
const handleFieldUpdate = (field) => {
|
||||||
// fieldErrors.value[field] = ''
|
fieldErrors.value[field] = ''
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -118,7 +120,7 @@ const handleFieldUpdate = (field) => {
|
|||||||
:error-messages="fieldErrors.email"
|
:error-messages="fieldErrors.email"
|
||||||
label="Email"
|
label="Email"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@@update:modelValue="handleFieldUpdate('email')"
|
@update:modelValue="handleFieldUpdate('email')"
|
||||||
clearable
|
clearable
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
|
|||||||
243
pages/index.vue
243
pages/index.vue
@@ -1,202 +1,91 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ["auth"]
|
middleware: ["auth"],
|
||||||
|
path: '/:id?',
|
||||||
|
keepalive: true
|
||||||
})
|
})
|
||||||
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
|
|
||||||
import { nextTick } from 'vue'
|
|
||||||
|
|
||||||
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())
|
||||||
|
|
||||||
let ctrl
|
const loadConversation = async () => {
|
||||||
const abortFetch = () => {
|
const { data, error } = await useAuthFetch('/api/chat/conversations/' + route.params.id)
|
||||||
if (ctrl) {
|
if (!error.value) {
|
||||||
ctrl.abort()
|
conversation.value = Object.assign(conversation.value, data.value)
|
||||||
}
|
|
||||||
fetchingResponse.value = false
|
|
||||||
}
|
|
||||||
const fetchReply = async (message, parentMessageId) => {
|
|
||||||
ctrl = new AbortController()
|
|
||||||
try {
|
|
||||||
await fetchEventSource('/api/conversation/', {
|
|
||||||
signal: ctrl.signal,
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: currentModel.value,
|
|
||||||
openaiApiKey: openaiApiKey.value,
|
|
||||||
message: message,
|
|
||||||
parentMessageId: parentMessageId,
|
|
||||||
conversationId: currentConversation.value.id
|
|
||||||
}),
|
|
||||||
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) {
|
|
||||||
// console.log(message)
|
|
||||||
const event = message.event
|
|
||||||
const data = JSON.parse(message.data)
|
|
||||||
|
|
||||||
if (event === 'error') {
|
|
||||||
throw new Error(data.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentConversation.value.messages[currentConversation.value.messages.length - 1].is_bot) {
|
|
||||||
currentConversation.value.messages[currentConversation.value.messages.length - 1].message += data.content
|
|
||||||
} else {
|
|
||||||
currentConversation.value.messages.push({id: null, is_bot: true, message: data.content})
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollChatWindow()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
abortFetch()
|
|
||||||
showSnackbar(err.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentConversation = useConversion()
|
const loadMessage = async () => {
|
||||||
|
const { data, error } = await useAuthFetch('/api/chat/messages/?conversationId=' + route.params.id)
|
||||||
const grab = ref(null)
|
if (!error.value) {
|
||||||
const scrollChatWindow = () => {
|
conversation.value.messages = data.value
|
||||||
if (grab.value === null) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
grab.value.scrollIntoView({behavior: 'smooth'})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createNewConversation = () => {
|
||||||
const send = (message) => {
|
if (route.path !== '/') {
|
||||||
fetchingResponse.value = true
|
return navigateTo('/?new')
|
||||||
let parentMessageId = null
|
|
||||||
if (currentConversation.value.messages.length > 0) {
|
|
||||||
const lastMessage = currentConversation.value.messages[currentConversation.value.messages.length - 1]
|
|
||||||
if (lastMessage.is_bot && lastMessage.id !== null) {
|
|
||||||
parentMessageId = lastMessage.id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
currentConversation.value.messages.push({parentMessageId: parentMessageId, message: message})
|
conversation.value = Object.assign(getDefaultConversationData(), {
|
||||||
fetchReply(message, parentMessageId)
|
topic: $i18n.t('newConversation')
|
||||||
scrollChatWindow()
|
})
|
||||||
}
|
|
||||||
const stop = () => {
|
|
||||||
abortFetch()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const snackbar = ref(false)
|
|
||||||
const snackbarText = ref('')
|
|
||||||
const showSnackbar = (text) => {
|
|
||||||
snackbarText.value = text
|
|
||||||
snackbar.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (route.params.id) {
|
||||||
|
conversation.value.loadingMessages = true
|
||||||
|
await loadConversation()
|
||||||
|
await loadMessage()
|
||||||
|
conversation.value.loadingMessages = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const navTitle = computed(() => {
|
||||||
|
if (conversation.value && conversation.value.topic !== null) {
|
||||||
|
return conversation.value.topic === '' ? $i18n.t('defaultConversationTitle') : conversation.value.topic
|
||||||
|
}
|
||||||
|
return runtimeConfig.public.appName
|
||||||
|
})
|
||||||
|
|
||||||
|
onActivated(async () => {
|
||||||
|
if (route.path === '/' && route.query.new !== undefined) {
|
||||||
|
createNewConversation()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
</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-container>
|
|
||||||
<v-row>
|
|
||||||
<v-col
|
|
||||||
v-for="(message, index) in currentConversation.messages" :key="index"
|
|
||||||
cols="12"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="d-flex"
|
|
||||||
:class="message.is_bot ? 'justify-start mr-16' : 'justify-end ml-16'"
|
|
||||||
>
|
|
||||||
<v-card
|
|
||||||
:color="message.is_bot ? '' : 'primary'"
|
|
||||||
rounded="lg"
|
|
||||||
elevation="2"
|
|
||||||
>
|
|
||||||
<v-card-text>
|
|
||||||
<MsgContent :content="message.message" />
|
|
||||||
</v-card-text>
|
|
||||||
|
|
||||||
<!-- <v-card-actions-->
|
<v-toolbar-title>{{ navTitle }}</v-toolbar-title>
|
||||||
<!-- v-if="message.is_bot"-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <v-spacer></v-spacer>-->
|
|
||||||
<!-- <v-tooltip text="Copy">-->
|
|
||||||
<!-- <template v-slot:activator="{ props }">-->
|
|
||||||
<!-- <v-btn v-bind="props" icon="content_copy"></v-btn>-->
|
|
||||||
<!-- </template>-->
|
|
||||||
<!-- </v-tooltip>-->
|
|
||||||
<!-- </v-card-actions>-->
|
|
||||||
</v-card>
|
|
||||||
</div>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-container>
|
|
||||||
|
|
||||||
<div ref="grab" class="w-100" style="height: 200px;"></div>
|
<v-spacer></v-spacer>
|
||||||
</div>
|
|
||||||
<Welcome v-else />
|
|
||||||
<v-footer app class="d-flex flex-column">
|
|
||||||
<div class="px-md-16 w-100 d-flex align-center">
|
|
||||||
<v-btn
|
|
||||||
v-show="fetchingResponse"
|
|
||||||
icon="close"
|
|
||||||
title="stop"
|
|
||||||
class="mr-3"
|
|
||||||
@click="stop"
|
|
||||||
></v-btn>
|
|
||||||
<MsgEditor :send-message="send" :disabled="fetchingResponse" :loading="fetchingResponse" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="px-4 py-2 text-disabled text-caption font-weight-light text-center w-100">
|
<v-btn
|
||||||
© {{ new Date().getFullYear() }} {{ runtimeConfig.public.appName }}
|
:title="$t('newConversation')"
|
||||||
</div>
|
icon="add"
|
||||||
</v-footer>
|
@click="createNewConversation"
|
||||||
<v-snackbar
|
class="d-md-none"
|
||||||
v-model="snackbar"
|
></v-btn>
|
||||||
multi-line
|
<v-btn
|
||||||
location="top"
|
variant="outlined"
|
||||||
>
|
class="text-none d-none d-md-block"
|
||||||
{{ snackbarText }}
|
@click="createNewConversation"
|
||||||
|
>
|
||||||
|
{{ $t('newConversation') }}
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
<template v-slot:actions>
|
</v-app-bar>
|
||||||
<v-btn
|
|
||||||
color="red"
|
<v-main>
|
||||||
variant="text"
|
<Welcome v-if="!route.params.id && conversation.messages.length === 0" />
|
||||||
@click="snackbar = false"
|
<Conversation :conversation="conversation" />
|
||||||
>
|
</v-main>
|
||||||
Close
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
</v-snackbar>
|
|
||||||
</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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { createVuetify } from 'vuetify'
|
import { createVuetify } from 'vuetify'
|
||||||
import { aliases, md } from 'vuetify/iconsets/md'
|
import { aliases, md } from 'vuetify/iconsets/md'
|
||||||
import * as components from 'vuetify/components'
|
import * as components from 'vuetify/components'
|
||||||
|
import { md3 } from 'vuetify/blueprints'
|
||||||
// import * as directives from 'vuetify/directives'
|
// import * as directives from 'vuetify/directives'
|
||||||
|
|
||||||
export default defineNuxtPlugin(nuxtApp => {
|
export default defineNuxtPlugin(nuxtApp => {
|
||||||
const vuetify = createVuetify({
|
const vuetify = createVuetify({
|
||||||
ssr: true,
|
ssr: true,
|
||||||
|
blueprint: md3,
|
||||||
icons: {
|
icons: {
|
||||||
defaultSet: 'md',
|
defaultSet: 'md',
|
||||||
aliases,
|
aliases,
|
||||||
|
|||||||
3
public/icon-black.svg
Normal file
3
public/icon-black.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="900" height="900" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M504.908 750H839.476C850.103 750.001 860.542 747.229 869.745 741.963C878.948 736.696 886.589 729.121 891.9 719.999C897.211 710.876 900.005 700.529 900 689.997C899.995 679.465 897.193 669.12 891.873 660.002L667.187 274.289C661.876 265.169 654.237 257.595 645.036 252.329C635.835 247.064 625.398 244.291 614.773 244.291C604.149 244.291 593.711 247.064 584.511 252.329C575.31 257.595 567.67 265.169 562.36 274.289L504.908 372.979L392.581 179.993C387.266 170.874 379.623 163.301 370.42 158.036C361.216 152.772 350.777 150 340.151 150C329.525 150 319.086 152.772 309.883 158.036C300.679 163.301 293.036 170.874 287.721 179.993L8.12649 660.002C2.80743 669.12 0.00462935 679.465 5.72978e-06 689.997C-0.00461789 700.529 2.78909 710.876 8.10015 719.999C13.4112 729.121 21.0523 736.696 30.255 741.963C39.4576 747.229 49.8973 750.001 60.524 750H270.538C353.748 750 415.112 713.775 457.336 643.101L559.849 467.145L614.757 372.979L779.547 655.834H559.849L504.908 750ZM267.114 655.737L120.551 655.704L340.249 278.586L449.87 467.145L376.474 593.175C348.433 639.03 316.577 655.737 267.114 655.737Z" fill="#0C0C0D"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/icon.png
Normal file
BIN
public/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
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
|
||||||
@@ -1,6 +1,29 @@
|
|||||||
|
|
||||||
export const STORAGE_KEY = {
|
export const STORAGE_KEY = {
|
||||||
OPENAI_MODELS: 'openai_models',
|
MODELS: 'models',
|
||||||
CURRENT_OPENAI_MODEL: 'current_openai_model',
|
CURRENT_MODEL: 'current_model',
|
||||||
OPENAI_API_KEY: 'openai_api_key',
|
OPENAI_API_KEY: 'openai_api_key',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MODELS = {
|
||||||
|
'gpt-3.5-turbo': {
|
||||||
|
name: 'gpt-3.5-turbo',
|
||||||
|
frequency_penalty: 0.0,
|
||||||
|
presence_penalty: 0.0,
|
||||||
|
total_tokens: 4096,
|
||||||
|
max_tokens: 1000,
|
||||||
|
temperature: 0.7,
|
||||||
|
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,38 +17,63 @@ 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 } = useNuxtApp()
|
||||||
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')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
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) => {
|
||||||
|
const result = {};
|
||||||
|
for (let i = 0; i < list.length; i++) {
|
||||||
|
const item = list[i];
|
||||||
|
result[item.name] = item.value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchSystemSettings = async () => {
|
||||||
|
const { data, error } = await useAuthFetch('/api/chat/settings/', {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
if (!error.value) {
|
||||||
|
const settings = useSettings()
|
||||||
|
settings.value = transformData(data.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchUser = async () => {
|
||||||
|
return useMyFetch('/api/account/user/')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setUser = (userData) => {
|
||||||
|
const user = useUser()
|
||||||
|
user.value = userData
|
||||||
|
}
|
||||||
|
|
||||||
|
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,35 +9,32 @@ 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_OPENAI_MODEL = 'text-davinci-003'
|
|
||||||
|
|
||||||
export const setModels = (val) => {
|
export const setModels = (val) => {
|
||||||
const models = useModels()
|
const models = useModels()
|
||||||
set(STORAGE_KEY.OPENAI_MODELS, val)
|
set(STORAGE_KEY.MODELS, val)
|
||||||
models.value = val
|
models.value = val
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStoredModels = () => {
|
// export const getStoredModels = () => {
|
||||||
let models = get(STORAGE_KEY.OPENAI_MODELS)
|
// let models = get(STORAGE_KEY.MODELS)
|
||||||
if (!models) {
|
// if (!models) {
|
||||||
models = [DEFAULT_OPENAI_MODEL]
|
// models = [DEFAULT_MODEL]
|
||||||
}
|
// }
|
||||||
return models
|
// return models
|
||||||
}
|
// }
|
||||||
|
|
||||||
export const setCurrentModel = (val) => {
|
export const saveCurrentModel = (val) => {
|
||||||
const model = useCurrentModel()
|
set(STORAGE_KEY.CURRENT_MODEL, val)
|
||||||
set(STORAGE_KEY.CURRENT_OPENAI_MODEL, val)
|
|
||||||
model.value = val
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCurrentModel = () => {
|
export const getCurrentModel = () => {
|
||||||
let model = get(STORAGE_KEY.CURRENT_OPENAI_MODEL)
|
let model = get(STORAGE_KEY.CURRENT_MODEL)
|
||||||
if (!model) {
|
if (!model) {
|
||||||
model = DEFAULT_OPENAI_MODEL
|
model = MODELS[DEFAULT_MODEL_NAME]
|
||||||
}
|
}
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user