Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
298d7c1bda | ||
|
|
8e27487cbb | ||
|
|
a91f1b1348 | ||
|
|
63b95c2ce2 | ||
|
|
03512e8c7e | ||
|
|
002db29717 |
39
README.md
39
README.md
@@ -1,10 +1,10 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="demo" src="./demos/demo.gif?v=1">
|
<img alt="demo" src="./demos/demo.png?v=1">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# ChatGPT UI
|
# ChatGPT UI
|
||||||
|
|
||||||
A web client for ChatGPT, using OpenAI's API.
|
A web client for ChatGPT, using OpenAI's API. Provides Docker images and also supports quick deployment to servers using shell scripts.
|
||||||
|
|
||||||
## 📢Updates
|
## 📢Updates
|
||||||
|
|
||||||
@@ -12,6 +12,9 @@ A web client for ChatGPT, using OpenAI's API.
|
|||||||
<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`
|
||||||
|
|
||||||
|
**🎉🎉🎉Provide a shell script that can be used to quickly deploy the service to server** [Quick start](#one-click-depolyment)
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details open>
|
<details open>
|
||||||
@@ -30,6 +33,30 @@ Version 2 introduces the following new features:
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
## 🚀 One-click deployment <a name="one-click-depolyment"></a>
|
||||||
|
|
||||||
|
Note: This script has only been tested on Ubuntu Server 22.04 LTS.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash <(curl -Ls https://raw.githubusercontent.com/WongSaang/chatgpt-ui/main/deployment.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
> If you have a domain name, you can point it to the server's IP address using DNS resolution. Of course, using the server's IP address directly is also possible.
|
||||||
|
> During the script's execution, you will be prompted to enter a domain name. If you do not have a domain name, you can enter the server's IP address directly.
|
||||||
|
|
||||||
|
### After the deployment is complete
|
||||||
|
|
||||||
|
Access `http(s)://your.domain:9000/admin` / IP `http(s)://123.123.123.123:9000/admin` to log in to the administration panel.
|
||||||
|
|
||||||
|
Default superuser: `admin`
|
||||||
|
|
||||||
|
Default password: `password`
|
||||||
|
|
||||||
|
Before you can start chatting, you need to add an OpenAI API key. In the Settings model, add a record with the name `openai_api_key` and the value as your API key.
|
||||||
|
|
||||||
|
Now you can access the web client at `http(s)://your.domain` or `http://123.123.123.123` to start chatting.
|
||||||
|
|
||||||
|
🎉🎉🎉 Enjoy it!
|
||||||
|
|
||||||
## Quick start with Docker Compose
|
## Quick start with Docker Compose
|
||||||
|
|
||||||
@@ -53,10 +80,18 @@ services:
|
|||||||
backend-wsgi-server:
|
backend-wsgi-server:
|
||||||
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
|
||||||
#- 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.
|
||||||
- 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
|
||||||
|
# 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
|
||||||
ports:
|
ports:
|
||||||
- '8000:8000'
|
- '8000:8000'
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const contentHtml = computed(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-html="contentHtml"
|
v-html="contentHtml"
|
||||||
|
class="text-body-1 text-justify"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
BIN
demos/demo.gif
BIN
demos/demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 143 KiB |
BIN
demos/demo.png
Normal file
BIN
demos/demo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
48
deployment.sh
Normal file
48
deployment.sh
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
read -p "Please enter a resolved domain name: " domain
|
||||||
|
|
||||||
|
if [[ $(which docker) ]]; then
|
||||||
|
echo "Docker is already installed"
|
||||||
|
else
|
||||||
|
echo "Docker is not installed, installing now..."
|
||||||
|
|
||||||
|
sudo apt-get update
|
||||||
|
|
||||||
|
sudo apt-get install -y \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
gnupg \
|
||||||
|
lsb-release
|
||||||
|
|
||||||
|
sudo mkdir -m 0755 -p /etc/apt/keyrings
|
||||||
|
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||||
|
|
||||||
|
echo \
|
||||||
|
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
|
||||||
|
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
|
||||||
|
sudo apt-get update
|
||||||
|
|
||||||
|
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
fi
|
||||||
|
if [[ $(which docker-compose) ]]; then
|
||||||
|
echo "Docker Compose is already installed"
|
||||||
|
else
|
||||||
|
echo "Docker Compose is not installed, installing now..."
|
||||||
|
|
||||||
|
sudo curl -L "https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
|
||||||
|
sudo chmod +x /usr/local/bin/docker-compose
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Downloading configuration files..."
|
||||||
|
|
||||||
|
sudo curl -L "https://raw.githubusercontent.com/WongSaang/chatgpt-ui/main/docker-compose.yml" -o docker-compose.yml
|
||||||
|
|
||||||
|
echo "Starting services..."
|
||||||
|
|
||||||
|
sudo APP_DOMAIN="${domain}:9000" docker-compose up -d
|
||||||
|
|
||||||
|
echo "Done"
|
||||||
@@ -13,6 +13,7 @@ services:
|
|||||||
backend-wsgi-server:
|
backend-wsgi-server:
|
||||||
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||||
environment:
|
environment:
|
||||||
|
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000}
|
||||||
# - DB_URL=postgres://postgres:postgrespw@localhost:49153/chatgpt # If this parameter is not set, the built-in Sqlite will be used by default. It should be noted that if you do not connect to an external database, the data will be lost after the container is destroyed.
|
# - 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
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"followSystem": "Follow system",
|
"followSystem": "Follow system",
|
||||||
"themeMode": "Theme Mode",
|
"themeMode": "Theme Mode",
|
||||||
"feedback": "Feedback",
|
"feedback": "Feedback",
|
||||||
|
"clearConversations": "Clear conversations",
|
||||||
"roles": {
|
"roles": {
|
||||||
"me": "Me",
|
"me": "Me",
|
||||||
"ai": "AI"
|
"ai": "AI"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"followSystem": "跟随系统",
|
"followSystem": "跟随系统",
|
||||||
"themeMode": "主题模式",
|
"themeMode": "主题模式",
|
||||||
"feedback": "反馈",
|
"feedback": "反馈",
|
||||||
|
"clearConversations": "清除会话",
|
||||||
"roles": {
|
"roles": {
|
||||||
"me": "我",
|
"me": "我",
|
||||||
"ai": "AI"
|
"ai": "AI"
|
||||||
|
|||||||
@@ -25,8 +25,62 @@ const setLang = (lang) => {
|
|||||||
|
|
||||||
const conversations = useConversions()
|
const conversations = useConversions()
|
||||||
|
|
||||||
onNuxtReady(async () => {
|
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()
|
conversations.value = await getConversions()
|
||||||
|
loadingConversations.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onNuxtReady(async () => {
|
||||||
|
loadConversations()
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -39,23 +93,75 @@ onNuxtReady(async () => {
|
|||||||
v-model="drawer"
|
v-model="drawer"
|
||||||
>
|
>
|
||||||
<div class="px-2 py-2">
|
<div class="px-2 py-2">
|
||||||
|
<v-list>
|
||||||
|
<v-list-item>
|
||||||
<v-btn
|
<v-btn
|
||||||
block
|
block
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
prepend-icon="add"
|
prepend-icon="add"
|
||||||
size="large"
|
|
||||||
@click="createNewConversion()"
|
@click="createNewConversion()"
|
||||||
|
class="text-none"
|
||||||
>
|
>
|
||||||
New conversation
|
New conversation
|
||||||
</v-btn>
|
</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>
|
<v-list>
|
||||||
<v-list-item
|
<template
|
||||||
v-for="conversation in conversations"
|
v-for="(conversation, cIdx) in conversations"
|
||||||
:key="conversation.id"
|
:key="conversation.id"
|
||||||
:title="conversation.topic"
|
>
|
||||||
|
<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"
|
active-color="primary"
|
||||||
@click="openConversationMessages(conversation)"
|
@click="openConversationMessages(conversation)"
|
||||||
></v-list-item>
|
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>
|
</v-list>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -64,6 +170,47 @@ onNuxtReady(async () => {
|
|||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
<v-list>
|
<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
|
<v-menu
|
||||||
>
|
>
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ definePageMeta({
|
|||||||
middleware: ["auth"]
|
middleware: ["auth"]
|
||||||
})
|
})
|
||||||
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
|
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
|
||||||
|
import { nextTick } from 'vue'
|
||||||
|
|
||||||
const { $i18n, $auth } = useNuxtApp()
|
const { $i18n, $auth } = useNuxtApp()
|
||||||
const runtimeConfig = useRuntimeConfig()
|
const runtimeConfig = useRuntimeConfig()
|
||||||
@@ -49,7 +50,7 @@ const fetchReply = async (message, parentMessageId) => {
|
|||||||
onerror(err) {
|
onerror(err) {
|
||||||
throw err;
|
throw err;
|
||||||
},
|
},
|
||||||
onmessage(message) {
|
async onmessage(message) {
|
||||||
// console.log(message)
|
// console.log(message)
|
||||||
const event = message.event
|
const event = message.event
|
||||||
const data = JSON.parse(message.data)
|
const data = JSON.parse(message.data)
|
||||||
@@ -119,28 +120,49 @@ const showSnackbar = (text) => {
|
|||||||
snackbar.value = true
|
snackbar.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="currentConversation.messages.length > 0"
|
v-if="currentConversation.messages.length > 0"
|
||||||
ref="chatWindow"
|
ref="chatWindow"
|
||||||
>
|
|
||||||
<v-card
|
|
||||||
rounded="0"
|
|
||||||
elevation="0"
|
|
||||||
v-for="(conversation, index) in currentConversation.messages"
|
|
||||||
:key="index"
|
|
||||||
:variant="conversation.is_bot ? 'tonal' : 'text'"
|
|
||||||
>
|
>
|
||||||
<v-container>
|
<v-container>
|
||||||
<v-card-text class="text-caption text-disabled">{{ $t(`roles.${conversation.is_bot?'ai':'me'}`) }}</v-card-text>
|
<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>
|
<v-card-text>
|
||||||
<MsgContent :content="conversation.message" />
|
<MsgContent :content="message.message" />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-container>
|
|
||||||
<v-divider></v-divider>
|
<!-- <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>
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
<div ref="grab" class="w-100" style="height: 200px;"></div>
|
<div ref="grab" class="w-100" style="height: 200px;"></div>
|
||||||
</div>
|
</div>
|
||||||
<Welcome v-else />
|
<Welcome v-else />
|
||||||
|
|||||||
Reference in New Issue
Block a user