Compare commits

...

38 Commits

Author SHA1 Message Date
Rafi
8175f199d2 Support GPT-4 2023-03-27 22:17:19 +08:00
Wong Saang
f8c2f396c1 Merge pull request #70 from Paramon/main
#50 add platform to docker-compose
2023-03-25 21:03:50 +08:00
Andrii Paramonov
8217647df8 #50 add platform 2023-03-24 16:44:00 +02:00
Rafi
288c9eeeca Change the functionality of custom API key to be controlled by the admin panel to determine whether it is enabled or not. 2023-03-24 15:43:01 +08:00
Rafi
4d09ff7c8a Added env var NUXT_PUBLIC_CUSTOM_API_KEY to docs 2023-03-24 14:32:38 +08:00
Rafi
5fa059017c Support controlling whether to enable the API Key setting module through environment variables. 2023-03-24 14:22:45 +08:00
Rafi
323f10844b Modify the placeholder of the default prompt for web search to solve the problem of not providing web search results to ChatGPT 2023-03-24 11:22:51 +08:00
Rafi
ee035390db add default prompt to web search 2023-03-23 18:48:08 +08:00
Wong Saang
be743bf799 Update README.md 2023-03-23 17:13:34 +08:00
Wong Saang
a59f84f2bf Update README.md 2023-03-23 17:12:59 +08:00
Rafi
ed0cf2997d update readme 2023-03-23 17:07:49 +08:00
Rafi
7f00c74097 Added wsgi-server environment variable EMAIL_FROM to docker-compose.yml and readme 2023-03-23 15:31:26 +08:00
Rafi
f007417fa4 add update info to readme 2023-03-23 12:53:08 +08:00
Rafi
27c5e2a3ac Get settings from backend, added web search functionality 2023-03-23 11:45:56 +08:00
Rafi
e90dc0c12b web_search toolbar 2023-03-22 23:29:58 +08:00
Rafi
837fd8c9ff update readme 2023-03-22 17:26:22 +08:00
Rafi
ce0b1004f3 Remove the parent_message_id constraint 2023-03-22 16:17:46 +08:00
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
29 changed files with 2890 additions and 198 deletions

View File

@@ -1,14 +1,30 @@
<p align="center">
<img alt="demo" src="./demos/demo.gif?v=1">
</p>
<div align="center">
<h1>ChatGPT UI</h1>
</div>
[English](./README.md) | [中文](./docs/zh/README.md)
# ChatGPT UI
A ChatGPT web client that supports multiple users, multiple database connections for persistent data storage, supports i18n. Provides Docker images and quick deployment scripts.
https://user-images.githubusercontent.com/46235412/227156264-ca17ab17-999b-414f-ab06-3f75b5235bfe.mp4
## 📢Updates
<details open>
<summary><strong>2023-03-23</strong></summary>
Added web search capability to generate more relevant and up-to-date answers from ChatGPT!
This feature is off by default, you can turn it on in `Chat->Settings` in the admin panel, there is a record `open_web_search` in Settings, set its value to True.
</details>
<details open>
<summary><strong>2023-03-15</strong></summary>
Add "open_registration" setting option in the admin panel to control whether user registration is enabled. You can log in to the admin panel and find this setting option under `Chat->Setting`. The default value of this setting is `True` (allow user registration). If you do not need it, please change it to `False`.
</details>
<details open>
<summary><strong>2023-03-10</strong></summary>
@@ -100,12 +116,14 @@ services:
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name
- DJANGO_SUPERUSER_PASSWORD=password # default superuser password
- DJANGO_SUPERUSER_EMAIL=admin@example.com # default superuser email
- ACCOUNT_EMAIL_VERIFICATION=none # Determines the e-mail verification method during signup choose one of "none", "optional", or "mandatory". Default is "optional". If you don't need to verify the email, you can set it to "none".
# If you want to use the email verification function, you need to configure the following parameters
# - EMAIL_HOST=SMTP server address
# - EMAIL_PORT=SMTP server port
# - EMAIL_HOST_USER=
# - EMAIL_HOST_PASSWORD=
# - EMAIL_USE_TLS=True
# - EMAIL_FROM=no-reply@example.com #Default sender email address
ports:
- '8000:8000'
networks:
@@ -147,6 +165,16 @@ Before you can start chatting, you need to add an OpenAI API key. In the Setting
Now you can access the web client at `http(s)://your.domain` or `http://123.123.123.123` to start chatting.
## Donation
> If it is helpful to you, it is also helping me.
If you want to support me, Buy me a coffee ❤️ [https://www.buymeacoffee.com/WongSaang](https://www.buymeacoffee.com/WongSaang)
<p align="center">
<img height="150" src="https://github.com/WongSaang/chatgpt-ui/blob/main/demos/bmc_qr.png?raw=true"/>
</p>
## Development
### Setup
@@ -172,4 +200,4 @@ Build the application for production:
```bash
yarn build
```
```

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,195 @@
<script setup>
const dialog = ref(false)
const currentModel = useCurrentModel()
const availableModels = [
'gpt-3.5-turbo',
'gpt-4'
]
const currentModelDefault = ref(MODELS[currentModel.value.name])
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="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="currentModelDefault.total_tokens"
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="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="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 contentElm = ref(null)
watchEffect(() => {
contentHtml.value = props.content ? md.render(props.content) : ''
contentHtml.value = props.message.message ? md.render(props.message.message) : ''
})
const bindCopyCodeToButtons = () => {
@@ -54,11 +59,19 @@ onUpdated(() => {
</script>
<template>
<div
ref="contentElm"
v-html="contentHtml"
class="chat-msg-content"
></div>
<v-card
:color="message.is_bot ? '' : 'primary'"
rounded="lg"
elevation="2"
>
<v-card-text>
<div
ref="contentElm"
v-html="contentHtml"
class="chat-msg-content"
></div>
</v-card-text>
</v-card>
</template>
<style>

View File

@@ -1,17 +1,29 @@
<template>
<v-textarea
v-model="message"
:label="$t('writeAMessage')"
:placeholder="hint"
rows="1"
:auto-grow="autoGrow"
:disabled="disabled"
:loading="loading"
:hide-details="true"
append-inner-icon="send"
@keyup.enter.exact="enterOnly"
@click:appendInner="clickSendBtn"
></v-textarea>
<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>
<script>
@@ -39,7 +51,7 @@ export default {
message(val) {
const lines = val.split(/\r\n|\r|\n/).length;
if (lines > 8) {
this.rows = lines;
this.rows = 8;
this.autoGrow = false;
} else {
this.rows = 1;
@@ -65,7 +77,8 @@ export default {
clickSendBtn () {
this.send()
},
enterOnly () {
enterOnly (event) {
event.preventDefault();
if (!isMobile()) {
this.send()
}

View File

@@ -96,10 +96,12 @@ onMounted( () => {
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
icon="speaker_notes"
title="Common prompts"
class="mr-3"
></v-btn>
icon
>
<v-icon
icon="speaker_notes"
></v-icon>
</v-btn>
</template>
<v-container>

View File

@@ -15,26 +15,23 @@
</template>
<v-card>
<v-toolbar
dark
color="primary"
>
<v-btn
icon
dark
@click="dialog = false"
>
<v-icon>close</v-icon>
<v-icon icon="close"></v-icon>
</v-btn>
<v-toolbar-title>{{ $t('language') }}</v-toolbar-title>
<v-spacer></v-spacer>
<!-- <v-toolbar-items>-->
<!-- <v-btn-->
<!-- variant="text"-->
<!-- @click="dialog = false"-->
<!-- >-->
<!-- Save-->
<!-- </v-btn>-->
<!-- </v-toolbar-items>-->
<v-toolbar-items>
<v-btn
variant="text"
@click="dialog = false"
>
Save
</v-btn>
</v-toolbar-items>
</v-toolbar>
<v-list
>

View File

@@ -1,5 +1,5 @@
export const useModels = () => useState('models', () => getStoredModels())
// export const useModels = () => useState('models', () => getStoredModels())
export const useCurrentModel = () => useState('currentModel', () => getCurrentModel())
@@ -7,4 +7,6 @@ export const useApiKey = () => useState('apiKey', () => getStoredApiKey())
export const useConversion = () => useState('conversion', () => getDefaultConversionData())
export const useConversions = () => useState('conversions', () => [])
export const useConversions = () => useState('conversions', () => [])
export const useSettings = () => useState('settings', () => {})

BIN
demos/bmc_qr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
demos/demo.mp4 Normal file

Binary file not shown.

View File

@@ -65,6 +65,6 @@ sudo curl -L "https://raw.githubusercontent.com/WongSaang/chatgpt-ui/main/docker
echo "Starting services..."
sudo APP_DOMAIN="${APP_DOMAIN}:${SERVER_PORT}" CLIENT_PORT=${CLIENT_PORT} SERVER_PORT=${SERVER_PORT} WSGI_PORT=${WSGI_PORT} docker-compose up --pull -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"

View File

@@ -1,6 +1,7 @@
version: '3'
services:
client:
platform: linux/x86_64
image: wongsaang/chatgpt-ui-client:latest
environment:
- SERVER_DOMAIN=http://backend-web-server
@@ -15,6 +16,7 @@ services:
- chatgpt_ui_network
restart: always
backend-wsgi-server:
platform: linux/x86_64
image: wongsaang/chatgpt-ui-wsgi-server:latest
environment:
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000}
@@ -22,18 +24,21 @@ services:
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name
- DJANGO_SUPERUSER_PASSWORD=password # default superuser password
- DJANGO_SUPERUSER_EMAIL=admin@example.com # default superuser email
- ACCOUNT_EMAIL_VERIFICATION=${ACCOUNT_EMAIL_VERIFICATION:-none} # Determines the e-mail verification method during signup choose one of "none", "optional", or "mandatory". Default is "optional". If you don't need to verify the email, you can set it to "none".
# If you want to use the email verification function, you need to configure the following parameters
# - EMAIL_HOST=SMTP server address
# - EMAIL_PORT=SMTP server port
# - EMAIL_HOST_USER=
# - EMAIL_HOST_PASSWORD=
# - EMAIL_USE_TLS=True
# - EMAIL_FROM=no-reply@example.com #Default sender email address
ports:
- '${WSGI_PORT:-8000}:8000'
networks:
- chatgpt_ui_network
restart: always
backend-web-server:
platform: linux/x86_64
image: wongsaang/chatgpt-ui-web-server:latest
environment:
- BACKEND_URL=http://backend-wsgi-server:8000
@@ -47,4 +52,4 @@ services:
networks:
chatgpt_ui_network:
driver: bridge
driver: bridge

View File

@@ -1,14 +1,28 @@
<p align="center">
<img alt="demo" src="../../demos/demo.gif?v=1">
</p>
<div align="center">
<h1>ChatGPT UI</h1>
</div>
[English](../../README.md) | [中文](./docs/zh/README.md)
# ChatGPT UI
ChatGPT Web 客户端,支持多用户,支持 Mysql、PostgreSQL 等多种数据库连接进行数据持久化存储,支持多语言。提供 Docker 镜像和快速部署脚本。
https://user-images.githubusercontent.com/46235412/227156264-ca17ab17-999b-414f-ab06-3f75b5235bfe.mp4
## 📢 更新
<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 open>
<summary><strong>2023-03-10</strong></summary>
@@ -99,12 +113,14 @@ services:
- 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:
@@ -146,6 +162,16 @@ networks:
现在可以访问客户端地址 `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
@@ -171,4 +197,4 @@ Build the application for production:
```bash
yarn build
```
```

View File

@@ -18,10 +18,24 @@
"feedback": "Feedback",
"newConversation": "New conversation",
"clearConversations": "Clear conversations",
"modelParameters": "Model Parameters",
"model": "Model",
"temperature": "Temperature",
"topP": "Top P",
"frequencyPenalty": "Frequency Penalty",
"presencePenalty": "Presence Penalty",
"maxTokens": "Max Tokens",
"roles": {
"me": "Me",
"ai": "AI"
},
"edit": "Edit",
"copy": "Copy",
"copied": "Copied",
"delete": "Delete",
"signOut": "Sign out",
"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]",
"welcomeScreen": {
"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.",

View File

@@ -18,10 +18,24 @@
"feedback": "反馈",
"newConversation": "新的对话",
"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": "退出登录",
"webSearch": "网页搜索",
"webSearchDefaultPrompt": "网络搜索结果:\n\n[web_results]\n当前日期[current_date]\n\n说明使用提供的网络搜索结果对给定的查询写出全面的回复。确保在引用参考文献后使用 [[number](URL)] 符号进行引用结果. 如果提供的搜索结果涉及到多个具有相同名称的主题,请针对每个主题编写单独的答案。\n查询[query]",
"welcomeScreen": {
"introduction1": "是一个非官方的ChatGPT客户端但使用OpenAI的官方API",
"introduction2": "在使用本客户端之前您需要一个OpenAI API密钥。",

View File

@@ -1,7 +1,7 @@
<script setup>
import {useDisplay} from "vuetify";
const { $i18n } = useNuxtApp()
const { $i18n, $auth } = useNuxtApp()
const runtimeConfig = useRuntimeConfig()
const colorMode = useColorMode()
const drawer = ref(null)
@@ -84,12 +84,32 @@ const loadConversations = async () => {
const {mdAndUp} = useDisplay()
const drawerPermanent = computed(() => {
return mdAndUp.value
})
onNuxtReady(async () => {
const signOut = async () => {
const { data, error } = await useFetch('/api/account/logout/', {
method: 'POST'
})
if (!error.value) {
await $auth.logout()
}
}
const settings = useSettings()
const showApiKeySetting = ref(false)
watchEffect(() => {
if (settings.value) {
const settingsValue = toRaw(settings.value)
showApiKeySetting.value = settingsValue.open_api_key_setting && settingsValue.open_api_key_setting === 'True'
}
})
onMounted(async () => {
loadConversations()
loadSettings()
})
</script>
@@ -228,6 +248,12 @@ onNuxtReady(async () => {
</v-card>
</v-dialog>
<ApiKeyDialog
v-if="showApiKeySetting"
/>
<ModelParameters/>
<v-menu
>
<template v-slot:activator="{ props }">
@@ -259,6 +285,14 @@ onNuxtReady(async () => {
:title="$t('feedback')"
@click="feedback"
></v-list-item>
<v-list-item
rounded="xl"
prepend-icon="logout"
:title="$t('signOut')"
@click="signOut"
></v-list-item>
</v-list>
</div>
</template>
@@ -279,29 +313,12 @@ onNuxtReady(async () => {
@click="createNewConversion()"
></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-main>
<NuxtPage/>
</v-main>
</v-app>
</template>
@@ -316,4 +333,5 @@ onNuxtReady(async () => {
background-color: #999;
border-radius: 3px;
}
</style>

View File

@@ -1,5 +1,5 @@
// 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({
dev: false,
@@ -14,6 +14,7 @@ export default defineNuxtConfig({
appName: appName,
typewriter: false,
typewriterDelay: 50,
customApiKey: false
}
},
build: {
@@ -25,9 +26,20 @@ export default defineNuxtConfig({
'highlight.js/styles/panda-syntax-dark.css',
],
modules: [
'@kevinmarrec/nuxt-pwa',
'@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: {
strategy: 'no_prefix',
locales: [
@@ -54,7 +66,7 @@ export default defineNuxtConfig({
nitro: {
devProxy: {
"/api": {
target: "http://localhost:8000/api",
target: process.env.NUXT_DEV_SERVER ?? 'http://localhost:8000/api',
prependPath: true,
changeOrigin: true,
}

View File

@@ -8,8 +8,10 @@
"postinstall": "nuxt prepare"
},
"devDependencies": {
"@kevinmarrec/nuxt-pwa": "^0.17.0",
"@nuxtjs/color-mode": "^3.2.0",
"@nuxtjs/i18n": "^8.0.0-beta.9",
"@vite-pwa/nuxt": "^0.0.7",
"material-design-icons-iconfont": "^6.7.0",
"nuxt": "^3.2.0"
},

View File

@@ -45,24 +45,32 @@ onNuxtReady(() => {
elevation="0"
>
<div class="text-center">
<h2 class="text-h4">Verify your email</h2>
<p class="text-body-2 mt-5">
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.
</p>
<p v-if="errorMsg"
class="text-red"
>{{ errorMsg }}</p>
<v-btn
variant="text"
class="mt-5"
color="primary"
:loading="sending"
@click="resendEmail"
:disabled="resent"
>
{{ resent ? 'Resent' : 'Resend email'}}
</v-btn>
<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>
<p class="mt-5">
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.
</p>
<p v-if="errorMsg"
class="text-red"
>{{ errorMsg }}</p>
<v-btn
variant="text"
class="mt-5"
color="primary"
:loading="sending"
@click="resendEmail"
:disabled="resent"
>
{{ resent ? 'Resent' : 'Resend email'}}
</v-btn>
</div>
</div>
</v-card>
</v-col>

View File

@@ -75,7 +75,7 @@ const submit = async () => {
}
} else {
$auth.setUser(data.value.user)
navigateTo('/account/onboarding')
navigateTo('/account/onboarding?email_verification_required='+data.value.email_verification_required)
}
submitting.value = false

View File

@@ -5,7 +5,6 @@ definePageMeta({
middleware: ["auth"]
})
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
import { nextTick } from 'vue'
const { $i18n, $auth } = useNuxtApp()
const runtimeConfig = useRuntimeConfig()
@@ -49,8 +48,23 @@ const abortFetch = () => {
}
fetchingResponse.value = false
}
const fetchReply = async (message, parentMessageId) => {
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: currentConversation.value.id
}, webSearchParams)
try {
await fetchEventSource('/api/conversation/', {
signal: ctrl.signal,
@@ -59,13 +73,7 @@ const fetchReply = async (message, parentMessageId) => {
'accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: currentModel.value,
openaiApiKey: openaiApiKey.value,
message: message,
parentMessageId: parentMessageId,
conversationId: currentConversation.value.id
}),
body: JSON.stringify(data),
onopen(response) {
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
return;
@@ -82,12 +90,18 @@ const fetchReply = async (message, parentMessageId) => {
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);
abortFetch()
showSnackbar(data.error)
return;
}
if (event === 'userMessageId') {
currentConversation.value.messages[currentConversation.value.messages.length - 1].id = data.userMessageId
return;
}
if (event === 'done') {
@@ -126,15 +140,8 @@ const scrollChatWindow = () => {
const send = (message) => {
fetchingResponse.value = true
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})
fetchReply(message, parentMessageId)
currentConversation.value.messages.push({message: message})
fetchReply(message)
scrollChatWindow()
}
const stop = () => {
@@ -153,6 +160,24 @@ const usePrompt = (prompt) => {
editor.value.usePrompt(prompt)
}
const deleteMessage = (index) => {
currentConversation.value.messages.splice(index, 1)
}
const showWebSearchToggle = ref(false)
const enableWebSearch = ref(false)
const enableCustomApiKey = ref(false)
const settings = useSettings()
watchEffect(() => {
if (settings.value) {
const settingsValue = toRaw(settings.value)
showWebSearchToggle.value = settingsValue.open_web_search && settingsValue.open_web_search === 'True'
enableCustomApiKey.value = settingsValue.open_api_key_setting && settingsValue.open_api_key_setting === 'True'
}
})
</script>
<template>
@@ -167,29 +192,24 @@ const usePrompt = (prompt) => {
cols="12"
>
<div
class="d-flex"
:class="message.is_bot ? 'justify-start mr-16' : 'justify-end ml-16'"
class="d-flex align-center"
:class="message.is_bot ? 'justify-start' : 'justify-end'"
>
<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-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>
<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>
@@ -198,21 +218,36 @@ const usePrompt = (prompt) => {
<div ref="grab" class="w-100" style="height: 200px;"></div>
</div>
<Welcome v-else />
<v-footer app class="d-flex flex-column">
<div class="px-md-16 w-100 d-flex align-center">
<Prompt v-show="!fetchingResponse" :use-prompt="usePrompt" />
<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-footer app>
<div class="px-md-16 w-100 d-flex flex-column">
<div class="d-flex align-center">
<v-btn
v-show="fetchingResponse"
icon="close"
title="stop"
class="mr-3"
@click="stop"
></v-btn>
<MsgEditor ref="editor" :send-message="send" :disabled="fetchingResponse" :loading="fetchingResponse" />
</div>
<v-toolbar
density="comfortable"
color="transparent"
>
<Prompt v-show="!fetchingResponse" :use-prompt="usePrompt" />
<v-switch
v-if="showWebSearchToggle"
v-model="enableWebSearch"
hide-details
color="primary"
:label="$t('webSearch')"
></v-switch>
<v-spacer></v-spacer>
</v-toolbar>
<div class="px-4 py-2 text-disabled text-caption font-weight-light text-center w-100">
© {{ new Date().getFullYear() }} {{ runtimeConfig.public.appName }}
<!-- <div class="py-2 text-disabled text-caption font-weight-light text-center">-->
<!-- © {{ new Date().getFullYear() }} {{ runtimeConfig.public.appName }}-->
<!-- </div>-->
</div>
</v-footer>
<v-snackbar

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,29 @@
export const STORAGE_KEY = {
OPENAI_MODELS: 'openai_models',
CURRENT_OPENAI_MODEL: 'current_openai_model',
MODELS: 'models',
CURRENT_MODEL: 'current_model',
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'

View File

@@ -50,4 +50,23 @@ export const genTitle = async (conversationId) => {
return data.value.title
}
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 loadSettings = async () => {
const settings = useSettings()
const { data, error } = await useAuthFetch('/api/chat/settings/', {
method: 'GET'
})
if (!error.value) {
settings.value = transformData(data.value)
}
}

View File

@@ -1,3 +1,4 @@
import {MODELS} from "~/utils/enums";
const get = (key) => {
let val = localStorage.getItem(key)
@@ -11,32 +12,28 @@ const set = (key, val) => {
localStorage.setItem(key, JSON.stringify(val))
}
const DEFAULT_OPENAI_MODEL = 'text-davinci-003'
export const setModels = (val) => {
const models = useModels()
set(STORAGE_KEY.OPENAI_MODELS, val)
set(STORAGE_KEY.MODELS, val)
models.value = val
}
export const getStoredModels = () => {
let models = get(STORAGE_KEY.OPENAI_MODELS)
if (!models) {
models = [DEFAULT_OPENAI_MODEL]
}
return models
}
// export const getStoredModels = () => {
// let models = get(STORAGE_KEY.MODELS)
// if (!models) {
// models = [DEFAULT_MODEL]
// }
// return models
// }
export const setCurrentModel = (val) => {
const model = useCurrentModel()
set(STORAGE_KEY.CURRENT_OPENAI_MODEL, val)
model.value = val
export const saveCurrentModel = (val) => {
set(STORAGE_KEY.CURRENT_MODEL, val)
}
export const getCurrentModel = () => {
let model = get(STORAGE_KEY.CURRENT_OPENAI_MODEL)
let model = get(STORAGE_KEY.CURRENT_MODEL)
if (!model) {
model = DEFAULT_OPENAI_MODEL
model = MODELS[DEFAULT_MODEL_NAME]
}
return model
}

2212
yarn.lock

File diff suppressed because it is too large Load Diff