Compare commits

..

27 Commits

Author SHA1 Message Date
Rafi
1ff1c46e37 Fix the bug of being unable to delete messages. 2023-03-22 15:55:06 +08:00
Rafi
afa3e499dc add DEBUT_PWA env variable 2023-03-22 14:12:49 +08:00
Rafi
70ce5746bc Merge remote-tracking branch 'origin/main' into main
# Conflicts:
#	nuxt.config.ts
#	yarn.lock
2023-03-22 13:50:53 +08:00
Rafi
35d4292d29 Import the @kevinmarrec/nuxt-pwa module to fix the related bugs of PWA feature. 2023-03-21 22:13:02 +08:00
Rafi
8bbc44e7bf update nuxt.config.ts 2023-03-21 18:48:35 +08:00
Rafi
3dcb4be6e4 add robots.txt 2023-03-21 18:06:44 +08:00
Rafi
83f8072625 mv @vite-pwa/nuxt to devDependencies 2023-03-21 13:46:02 +08:00
Rafi
3992121b71 update: docker-compose.yml 2023-03-21 10:20:08 +08:00
Rafi
d08806f0c9 update readme 2023-03-20 22:15:13 +08:00
Rafi
85ac73efcc Add email verification requirement judgment after completing registration 2023-03-20 22:03:53 +08:00
Rafi
7cc5a6b347 Fix: the language settings dialog not displaying the close button. 2023-03-20 20:13:23 +08:00
Rafi
983e4d436d update: deployment.sh 2023-03-20 12:54:11 +08:00
Rafi
727826f1b1 Added a Sign-out button 2023-03-19 14:26:46 +08:00
Rafi
386659109c Added a new message action: delete 2023-03-19 13:49:12 +08:00
Rafi
bd9e8bf45e Optimize the editor and enhance the user experience. 2023-03-19 13:39:20 +08:00
Rafi
4e40530a8c Added a new message action: edit 2023-03-19 13:13:27 +08:00
Rafi
ea69a350f4 add environment variable NUXT_DEV_SERVER 2023-03-19 12:53:44 +08:00
Rafi
18a4251714 feat: Message actions 2023-03-17 18:27:07 +08:00
Rafi
878fda0054 Support configuring model parameters in the front-end and storing them in localStorage. 2023-03-17 17:01:18 +08:00
Rafi
1f3a025918 feature: pwa 2023-03-17 12:36:24 +08:00
Rafi
f9db3e5866 update readme 2023-03-15 11:23:35 +08:00
Rafi
c9615ed05c catch error detail in signup page 2023-03-15 11:12:05 +08:00
Rafi
0d4b6247e2 docker-compose.yml includes restart: always in each service 2023-03-14 16:04:59 +08:00
Rafi
c9c3431cff Update deployment.sh 2023-03-14 13:00:23 +08:00
Rafi
46abf3daa0 Solve the problem of not clearing the messages on the right side when deleting the current conversation. 2023-03-10 11:21:58 +08:00
Rafi
8dcd7f46b1 Add 2 environment variables to control the typewriter effect: 2023-03-10 10:57:45 +08:00
Rafi
33d9c392fa Resolve the issue of missing indentation for "ol" tag in message content. 2023-03-10 10:17:26 +08:00
24 changed files with 2796 additions and 175 deletions

View File

@@ -9,6 +9,23 @@
A ChatGPT web client that supports multiple users, multiple database connections for persistent data storage, supports i18n. Provides Docker images and quick deployment scripts. A ChatGPT web client that supports multiple users, multiple database connections for persistent data storage, supports i18n. Provides Docker images and quick deployment scripts.
## 📢Updates ## 📢Updates
<details open>
<summary><strong>2023-03-15</strong></summary>
Add "open_registration" setting option in the admin panel to control whether user registration is enabled. You can log in to the admin panel and find this setting option under `Chat->Setting`. The default value of this setting is `True` (allow user registration). If you do not need it, please change it to `False`.
</details>
<details open>
<summary><strong>2023-03-10</strong></summary>
Add 2 environment variables to control the typewriter effect:
- `NUXT_PUBLIC_TYPEWRITER=true` to enable/disable the typewriter effect
- `NUXT_PUBLIC_TYPEWRITER_DELAY=50` to set the delay time for each character in milliseconds.
</details>
<details open> <details open>
<summary><strong>2023-03-04</strong></summary> <summary><strong>2023-03-04</strong></summary>
@@ -19,7 +36,7 @@ A ChatGPT web client that supports multiple users, multiple database connections
</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).
@@ -73,6 +90,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' # App name
- NUXT_PUBLIC_TYPEWRITER=true # Enable typewriter effect, default is false
- NUXT_PUBLIC_TYPEWRITER_DELAY=100 # Typewriter effect delay time, default is 50ms
depends_on: depends_on:
- backend-web-server - backend-web-server
ports: ports:
@@ -88,6 +108,7 @@ services:
- 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

8
app.vue Normal file
View File

@@ -0,0 +1,8 @@
<template>
<div>
<NuxtLoadingIndicator />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>

View 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>

View File

@@ -0,0 +1,191 @@
<script setup>
const dialog = ref(false)
const currentModel = useCurrentModel()
const availableModels = [
DEFAULT_MODEL.name
]
watch(currentModel, (newVal, oldVal) => {
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="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="currentModel.max_tokens"
hide-details
single-line
density="compact"
type="number"
max="2048"
step="1"
style="width: 100px"
class="flex-grow-0"
></v-text-field>
</div>
</v-col>
<v-col cols="12">
<v-slider
v-model="currentModel.max_tokens"
:max="2048"
: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="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="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="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>

View File

@@ -12,14 +12,19 @@ const md = new MarkdownIt({
}, },
}) })
const props = defineProps(['content']) const props = defineProps({
message: {
type: Object,
required: true
}
})
const contentHtml = ref('') const contentHtml = ref('')
const contentElm = ref(null) const contentElm = ref(null)
watchEffect(() => { watchEffect(() => {
contentHtml.value = props.content ? md.render(props.content) : '' contentHtml.value = props.message.message ? md.render(props.message.message) : ''
}) })
const bindCopyCodeToButtons = () => { const bindCopyCodeToButtons = () => {
@@ -54,16 +59,24 @@ onUpdated(() => {
</script> </script>
<template> <template>
<v-card
:color="message.is_bot ? '' : 'primary'"
rounded="lg"
elevation="2"
>
<v-card-text>
<div <div
ref="contentElm" ref="contentElm"
v-html="contentHtml" v-html="contentHtml"
class="chat-msg-content" class="chat-msg-content"
></div> ></div>
</v-card-text>
</v-card>
</template> </template>
<style> <style>
.chat-msg-content ol { .chat-msg-content ol {
list-style-position: inside; padding-left: 2em;
} }
.hljs-code-container { .hljs-code-container {
border-radius: 3px; border-radius: 3px;

View File

@@ -1,17 +1,29 @@
<template> <template>
<div
class="flex-grow-1 d-flex align-center justify-space-between"
>
<v-textarea <v-textarea
v-model="message" v-model="message"
:label="$t('writeAMessage')" :label="$t('writeAMessage')"
:placeholder="hint" :placeholder="hint"
rows="1" :rows="rows"
max-rows="8"
:auto-grow="autoGrow" :auto-grow="autoGrow"
:disabled="disabled" :disabled="disabled"
:loading="loading" :loading="loading"
:hide-details="true" :hide-details="true"
append-inner-icon="send" clearable
@keyup.enter.exact="enterOnly" variant="outlined"
@click:appendInner="clickSendBtn" @keydown.enter.exact="enterOnly"
></v-textarea> ></v-textarea>
<v-btn
:disabled="loading"
icon="send"
title="Send"
class="ml-3"
@click="clickSendBtn"
></v-btn>
</div>
</template> </template>
<script> <script>
@@ -39,7 +51,7 @@ export default {
message(val) { message(val) {
const lines = val.split(/\r\n|\r|\n/).length; const lines = val.split(/\r\n|\r|\n/).length;
if (lines > 8) { if (lines > 8) {
this.rows = lines; this.rows = 8;
this.autoGrow = false; this.autoGrow = false;
} else { } else {
this.rows = 1; this.rows = 1;
@@ -65,7 +77,8 @@ export default {
clickSendBtn () { clickSendBtn () {
this.send() this.send()
}, },
enterOnly () { enterOnly (event) {
event.preventDefault();
if (!isMobile()) { if (!isMobile()) {
this.send() this.send()
} }

View File

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

View File

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

View File

@@ -4,12 +4,16 @@ 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'
- NUXT_PUBLIC_TYPEWRITER=true
- NUXT_PUBLIC_TYPEWRITER_DELAY=100
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:
image: wongsaang/chatgpt-ui-wsgi-server:latest image: wongsaang/chatgpt-ui-wsgi-server:latest
environment: environment:
@@ -18,6 +22,7 @@ services:
- 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
@@ -25,19 +30,21 @@ services:
# - EMAIL_HOST_PASSWORD= # - EMAIL_HOST_PASSWORD=
# - EMAIL_USE_TLS=True # - EMAIL_USE_TLS=True
ports: ports:
- '8000:8000' - '${WSGI_PORT:-8000}:8000'
networks: networks:
- chatgpt_ui_network - chatgpt_ui_network
restart: always
backend-web-server: backend-web-server:
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:

View File

@@ -9,6 +9,22 @@
ChatGPT Web 客户端,支持多用户,支持 Mysql、PostgreSQL 等多种数据库连接进行数据持久化存储,支持多语言。提供 Docker 镜像和快速部署脚本。 ChatGPT Web 客户端,支持多用户,支持 Mysql、PostgreSQL 等多种数据库连接进行数据持久化存储,支持多语言。提供 Docker 镜像和快速部署脚本。
## 📢 更新 ## 📢 更新
<details open>
<summary><strong>2023-03-15</strong></summary>
在管理后台增加 `open_registration` 设置项,用于控制是否开放用户注册。你可以登录管理后台,在 `Chat->Setting` 中看到这个设置项,默认是 `True` (允许用户注册),如果不需要,请改成 `False`
</details>
<details open>
<summary><strong>2023-03-10</strong></summary>
增加 2 个环境变量来控制打字机效果, 详见下方 docker-compose 配置的环境变量说明
- `NUXT_PUBLIC_TYPEWRITER` 是否开启打字机效果
- `NUXT_PUBLIC_TYPEWRITER_DELAY` 每个字的延迟时间,单位:毫秒
</details>
<details open> <details open>
<summary><strong>2023-03-04</strong></summary> <summary><strong>2023-03-04</strong></summary>
@@ -19,7 +35,7 @@ ChatGPT Web 客户端,支持多用户,支持 Mysql、PostgreSQL 等多种数
</details> </details>
<details open> <details>
<summary><strong>2023-02-24</strong></summary> <summary><strong>2023-02-24</strong></summary>
V2 是一个重要的更新,将后端功能分离为一个独立的项目,托管在 [chatgpt-ui-server](https://github.com/WongSaang/chatgpt-ui-server), 该项目使用基于 Python 的 Django 框架。 V2 是一个重要的更新,将后端功能分离为一个独立的项目,托管在 [chatgpt-ui-server](https://github.com/WongSaang/chatgpt-ui-server), 该项目使用基于 Python 的 Django 框架。
@@ -72,6 +88,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' # App 名称,默认为 ChatGPT UI
- NUXT_PUBLIC_TYPEWRITER=true # 是否启用打字机效果,默认关闭
- NUXT_PUBLIC_TYPEWRITER_DELAY=100 # 打字机效果的延迟时间,默认 50毫秒
depends_on: depends_on:
- backend-web-server - backend-web-server
ports: ports:
@@ -87,6 +106,7 @@ services:
- DJANGO_SUPERUSER_USERNAME=admin # 默认超级用户 - DJANGO_SUPERUSER_USERNAME=admin # 默认超级用户
- DJANGO_SUPERUSER_PASSWORD=password # 默认超级用户的密码 - DJANGO_SUPERUSER_PASSWORD=password # 默认超级用户的密码
- DJANGO_SUPERUSER_EMAIL=admin@example.com # 默认超级用户邮箱 - DJANGO_SUPERUSER_EMAIL=admin@example.com # 默认超级用户邮箱
- ACCOUNT_EMAIL_VERIFICATION=none # 邮箱验证方式,可选值: none, optional, mandatory. 默认为 optional。如果你不需要验证用户的邮箱可以设置为 none。
# 如果您想使用电子邮件验证功能,需要配置以下参数: # 如果您想使用电子邮件验证功能,需要配置以下参数:
# - EMAIL_HOST=SMTP server address # - EMAIL_HOST=SMTP server address
# - EMAIL_PORT=SMTP server port # - EMAIL_PORT=SMTP server port

View File

@@ -18,10 +18,22 @@
"feedback": "Feedback", "feedback": "Feedback",
"newConversation": "New conversation", "newConversation": "New conversation",
"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",
"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.",

View File

@@ -18,10 +18,22 @@
"feedback": "反馈", "feedback": "反馈",
"newConversation": "新的对话", "newConversation": "新的对话",
"clearConversations": "清除对话", "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": "退出登录",
"welcomeScreen": { "welcomeScreen": {
"introduction1": "是一个非官方的ChatGPT客户端但使用OpenAI的官方API", "introduction1": "是一个非官方的ChatGPT客户端但使用OpenAI的官方API",
"introduction2": "在使用本客户端之前您需要一个OpenAI API密钥。", "introduction2": "在使用本客户端之前您需要一个OpenAI API密钥。",

View File

@@ -1,9 +1,7 @@
<script setup> <script setup>
import {useConversions} from "../composables/states";
import {getConversions} from "../utils/helper";
import {useDisplay} from "vuetify"; import {useDisplay} from "vuetify";
const { $i18n } = useNuxtApp() const { $i18n, $auth } = useNuxtApp()
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
const colorMode = useColorMode() const colorMode = useColorMode()
const drawer = ref(null) const drawer = ref(null)
@@ -25,6 +23,7 @@ const setLang = (lang) => {
} }
const conversations = useConversions() const conversations = useConversions()
const currentConversation = useConversion()
const editingConversation = ref(null) const editingConversation = ref(null)
const deletingConversationIndex = ref(null) const deletingConversationIndex = ref(null)
@@ -54,6 +53,9 @@ const deleteConversation = async (index) => {
}) })
deletingConversationIndex.value = null deletingConversationIndex.value = null
if (!error.value) { if (!error.value) {
if (conversations.value[index].id === currentConversation.value.id) {
createNewConversion()
}
conversations.value.splice(index, 1) conversations.value.splice(index, 1)
} }
} }
@@ -86,6 +88,15 @@ const drawerPermanent = computed(() => {
return mdAndUp.value return mdAndUp.value
}) })
const signOut = async () => {
const { data, error } = await useFetch('/api/account/logout/', {
method: 'POST'
})
if (!error.value) {
await $auth.logout()
}
}
onNuxtReady(async () => { onNuxtReady(async () => {
loadConversations() loadConversations()
}) })
@@ -162,7 +173,7 @@ onNuxtReady(async () => {
icon="edit" icon="edit"
size="small" size="small"
variant="text" variant="text"
@click="editConversation(cIdx)" @click.stop="editConversation(cIdx)"
> >
</v-btn> </v-btn>
<v-btn <v-btn
@@ -170,7 +181,7 @@ onNuxtReady(async () => {
size="small" size="small"
variant="text" variant="text"
:loading="deletingConversationIndex === cIdx" :loading="deletingConversationIndex === cIdx"
@click="deleteConversation(cIdx)" @click.stop="deleteConversation(cIdx)"
> >
</v-btn> </v-btn>
</div> </div>
@@ -226,6 +237,8 @@ onNuxtReady(async () => {
</v-card> </v-card>
</v-dialog> </v-dialog>
<ModelParameters/>
<v-menu <v-menu
> >
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
@@ -257,6 +270,14 @@ onNuxtReady(async () => {
:title="$t('feedback')" :title="$t('feedback')"
@click="feedback" @click="feedback"
></v-list-item> ></v-list-item>
<v-list-item
rounded="xl"
prepend-icon="logout"
:title="$t('signOut')"
@click="signOut"
></v-list-item>
</v-list> </v-list>
</div> </div>
</template> </template>
@@ -277,29 +298,12 @@ onNuxtReady(async () => {
@click="createNewConversion()" @click="createNewConversion()"
></v-btn> ></v-btn>
<!-- <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-app-bar>
<v-main> <v-main>
<NuxtPage/> <NuxtPage/>
</v-main> </v-main>
</v-app> </v-app>
</template> </template>
@@ -314,4 +318,5 @@ onNuxtReady(async () => {
background-color: #999; background-color: #999;
border-radius: 3px; border-radius: 3px;
} }
</style> </style>

View File

@@ -11,7 +11,9 @@ export default defineNuxtConfig({
}, },
runtimeConfig: { runtimeConfig: {
public: { public: {
appName: appName appName: appName,
typewriter: false,
typewriterDelay: 50,
} }
}, },
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: [
@@ -52,7 +65,7 @@ export default defineNuxtConfig({
nitro: { nitro: {
devProxy: { devProxy: {
"/api": { "/api": {
target: "http://localhost:8000/api", target: process.env.NUXT_DEV_SERVER ?? 'http://localhost:8000/api',
prependPath: true, prependPath: true,
changeOrigin: true, changeOrigin: true,
} }

View File

@@ -8,8 +8,10 @@
"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",
"@vite-pwa/nuxt": "^0.0.7",
"material-design-icons-iconfont": "^6.7.0", "material-design-icons-iconfont": "^6.7.0",
"nuxt": "^3.2.0" "nuxt": "^3.2.0"
}, },

View File

@@ -45,8 +45,15 @@ onNuxtReady(() => {
elevation="0" elevation="0"
> >
<div class="text-center"> <div class="text-center">
<div v-if="route.query.email_verification_required && route.query.email_verification_required === 'none'">
<h2 class="text-h4">Your registration is successful</h2>
<p class="mt-5">
You can now <NuxtLink to="/account/signin">login</NuxtLink> to your account.
</p>
</div>
<div v-else>
<h2 class="text-h4">Verify your email</h2> <h2 class="text-h4">Verify your email</h2>
<p class="text-body-2 mt-5"> <p class="mt-5">
We've sent a verification email to <strong>{{ $auth.user.email }}</strong>. <br> We've sent a verification email to <strong>{{ $auth.user.email }}</strong>. <br>
Please check your inbox and click the link to verify your email address. Please check your inbox and click the link to verify your email address.
</p> </p>
@@ -64,6 +71,7 @@ onNuxtReady(() => {
{{ resent ? 'Resent' : 'Resend email'}} {{ resent ? 'Resent' : 'Resend email'}}
</v-btn> </v-btn>
</div> </div>
</div>
</v-card> </v-card>
</v-col> </v-col>
</v-row> </v-row>

View File

@@ -66,12 +66,16 @@ const submit = async () => {
if (error.value.data.non_field_errors) { if (error.value.data.non_field_errors) {
errorMsg.value = error.value.data.non_field_errors[0] errorMsg.value = error.value.data.non_field_errors[0]
} }
} else {
if (error.value.data.detail) {
errorMsg.value = error.value.data.detail
} else { } else {
errorMsg.value = 'Something went wrong. Please try again.' errorMsg.value = 'Something went wrong. Please try again.'
} }
}
} else { } else {
$auth.setUser(data.value.user) $auth.setUser(data.value.user)
navigateTo('/account/onboarding') navigateTo('/account/onboarding?email_verification_required='+data.value.email_verification_required)
} }
submitting.value = false submitting.value = false

View File

@@ -6,6 +6,7 @@ definePageMeta({
}) })
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source' import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
import { nextTick } from 'vue' import { nextTick } from 'vue'
import MessageActions from "~/components/MessageActions.vue";
const { $i18n, $auth } = useNuxtApp() const { $i18n, $auth } = useNuxtApp()
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
@@ -24,19 +25,22 @@ const processMessageQueue = () => {
} }
isProcessingQueue = true isProcessingQueue = true
const nextMessage = messageQueue.shift() const nextMessage = messageQueue.shift()
if (runtimeConfig.public.typewriter) {
let wordIndex = 0;
const intervalId = setInterval(() => {
currentConversation.value.messages[currentConversation.value.messages.length - 1].message += nextMessage[wordIndex]
wordIndex++
if (wordIndex === nextMessage.length) {
clearInterval(intervalId)
isProcessingQueue = false
processMessageQueue()
}
}, runtimeConfig.public.typewriterDelay)
} else {
currentConversation.value.messages[currentConversation.value.messages.length - 1].message += nextMessage currentConversation.value.messages[currentConversation.value.messages.length - 1].message += nextMessage
isProcessingQueue = false isProcessingQueue = false
processMessageQueue() processMessageQueue()
// let wordIndex = 0; }
// const intervalId = setInterval(() => {
// currentConversation.value.messages[currentConversation.value.messages.length - 1].message += nextMessage[wordIndex]
// wordIndex++
// if (wordIndex === nextMessage.length) {
// clearInterval(intervalId)
// isProcessingQueue = false
// processMessageQueue()
// }
// }, 50)
} }
let ctrl let ctrl
@@ -48,6 +52,14 @@ const abortFetch = () => {
} }
const fetchReply = async (message, parentMessageId) => { const fetchReply = async (message, parentMessageId) => {
ctrl = new AbortController() ctrl = new AbortController()
const data = Object.assign({}, currentModel.value, {
openaiApiKey: openaiApiKey.value,
message: message,
// parentMessageId: parentMessageId,
conversationId: currentConversation.value.id
})
try { try {
await fetchEventSource('/api/conversation/', { await fetchEventSource('/api/conversation/', {
signal: ctrl.signal, signal: ctrl.signal,
@@ -56,13 +68,7 @@ const fetchReply = async (message, parentMessageId) => {
'accept': 'application/json', 'accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify(data),
model: currentModel.value,
openaiApiKey: openaiApiKey.value,
message: message,
parentMessageId: parentMessageId,
conversationId: currentConversation.value.id
}),
onopen(response) { onopen(response) {
if (response.ok && response.headers.get('content-type') === EventStreamContentType) { if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
return; return;
@@ -87,6 +93,13 @@ const fetchReply = async (message, parentMessageId) => {
throw new Error(data.error); throw new Error(data.error);
} }
if (event === 'userMessageId') {
console.log(currentConversation.value.messages[currentConversation.value.messages.length - 1])
currentConversation.value.messages[currentConversation.value.messages.length - 1].id = data.userMessageId
console.log(currentConversation.value.messages[currentConversation.value.messages.length - 1])
return;
}
if (event === 'done') { if (event === 'done') {
if (currentConversation.value.id === null) { if (currentConversation.value.id === null) {
currentConversation.value.id = data.conversationId currentConversation.value.id = data.conversationId
@@ -150,6 +163,10 @@ const usePrompt = (prompt) => {
editor.value.usePrompt(prompt) editor.value.usePrompt(prompt)
} }
const deleteMessage = (index) => {
currentConversation.value.messages.splice(index, 1)
}
</script> </script>
<template> <template>
@@ -164,29 +181,24 @@ const usePrompt = (prompt) => {
cols="12" cols="12"
> >
<div <div
class="d-flex" class="d-flex align-center"
:class="message.is_bot ? 'justify-start mr-16' : 'justify-end ml-16'" :class="message.is_bot ? 'justify-start' : 'justify-end'"
> >
<v-card <MessageActions
:color="message.is_bot ? '' : 'primary'" v-if="!message.is_bot"
rounded="lg" :message="message"
elevation="2" :message-index="index"
> :use-prompt="usePrompt"
<v-card-text> :delete-message="deleteMessage"
<MsgContent :content="message.message" /> />
</v-card-text> <MsgContent :message="message" />
<MessageActions
<!-- <v-card-actions--> v-if="message.is_bot"
<!-- v-if="message.is_bot"--> :message="message"
<!-- >--> :message-index="index"
<!-- <v-spacer></v-spacer>--> :use-prompt="usePrompt"
<!-- <v-tooltip text="Copy">--> :delete-message="deleteMessage"
<!-- <template v-slot:activator="{ props }">--> />
<!-- <v-btn v-bind="props" icon="content_copy"></v-btn>-->
<!-- </template>-->
<!-- </v-tooltip>-->
<!-- </v-card-actions>-->
</v-card>
</div> </div>
</v-col> </v-col>
</v-row> </v-row>

3
public/icon-black.svg Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Allow: /

View File

@@ -1,6 +1,15 @@
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 DEFAULT_MODEL = {
name: 'gpt-3.5-turbo',
frequency_penalty: 0.0,
presence_penalty: 0.0,
max_tokens: 1000,
temperature: 0.7,
top_p: 1.0
}

View File

@@ -11,32 +11,28 @@ const set = (key, val) => {
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 = DEFAULT_MODEL
} }
return model return model
} }

2217
yarn.lock

File diff suppressed because it is too large Load Diff