Compare commits

..

36 Commits

Author SHA1 Message Date
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
Rafi
bb17cdd123 Using markdown-it instead of marked as the markdown parser significantly improves the flickering issue during message rendering. 2023-03-09 23:35:56 +08:00
Rafi
4cfc9f4aea Temporarily disable the typewriter effect and improve the highlight method 2023-03-09 23:03:20 +08:00
Rafi
cd50086c1e update readme 2023-03-09 18:24:55 +08:00
Rafi
7e5498f779 update demog 2023-03-09 18:23:26 +08:00
Rafi
d933236a5d update demog 2023-03-09 18:23:11 +08:00
Rafi
0be2d45cd5 update demo 2023-03-09 18:22:09 +08:00
Rafi
e24ad26d99 update demo 2023-03-09 18:21:59 +08:00
Rafi
052f5299a0 Add frequently used prompt function. 2023-03-09 17:39:45 +08:00
Rafi
8340edbf40 Add typewriter effect to the messages of the model. 2023-03-09 15:05:40 +08:00
Rafi
7bff84638e update demo.png 2023-03-08 16:43:32 +08:00
Rafi
54660706e3 update demo.png 2023-03-08 16:41:58 +08:00
Rafi
a8acfeea58 Improve drawer width and conversation list style. 2023-03-08 16:22:22 +08:00
Rafi
85fc57e2b2 Display code language in code block, add code copy function 2023-03-08 16:09:34 +08:00
Rafi
fe4740b7a2 update layout 2023-03-08 11:33:23 +08:00
Rafi
2210dfcb98 update layout 2023-03-08 11:26:21 +08:00
Rafi
19794016fd update readme 2023-03-08 10:55:25 +08:00
Rafi
ce348c0f38 update the translation 2023-03-08 10:42:28 +08:00
Rafi
f251b16afe Add the 'clearable' attribute to the input on the signin page. Add the 'type' attribute to the password input and add a button to show/hide the password. 2023-03-08 10:37:00 +08:00
Rafi
4f32ef69b2 Add Chinese Readme 2023-03-07 21:21:06 +08:00
Rafi
e354a9490f Add Chinese Readme 2023-03-07 21:15:59 +08:00
Rafi
3d2c041cc2 update demo 2023-03-07 18:17:19 +08:00
Rafi
17588443e6 update readme 2023-03-07 18:02:11 +08:00
Rafi
298d7c1bda update readme 2023-03-07 17:48:18 +08:00
Rafi
8e27487cbb feat: Conversation editing and deletion 2023-03-07 17:39:49 +08:00
Rafi
a91f1b1348 update readme 2023-03-04 23:39:49 +08:00
Rafi
63b95c2ce2 update deployment.sh 2023-03-04 23:02:12 +08:00
Rafi
03512e8c7e Add a deployment script 2023-03-04 22:52:26 +08:00
24 changed files with 3270 additions and 115 deletions

View File

@@ -1,35 +1,81 @@
<p align="center">
<img alt="demo" src="./demos/demo.png?v=1">
<img alt="demo" src="./demos/demo.gif?v=1">
</p>
[English](./README.md) | [中文](./docs/zh/README.md)
# ChatGPT UI
A web client for ChatGPT, using OpenAI's API.
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
<details open>
<summary><strong>2023-03-15</strong></summary>
Add "open_registration" setting option in the admin panel to control whether user registration is enabled. You can log in to the admin panel and find this setting option under `Chat->Setting`. The default value of this setting is `True` (allow user registration). If you do not need it, please change it to `False`.
</details>
<details open>
<summary><strong>2023-03-10</strong></summary>
Add 2 environment variables to control the typewriter effect:
- `NUXT_PUBLIC_TYPEWRITER=true` to enable/disable the typewriter effect
- `NUXT_PUBLIC_TYPEWRITER_DELAY=50` to set the delay time for each character in milliseconds.
</details>
<details open>
<summary><strong>2023-03-04</strong></summary>
**Update to the latest official chat model ** `gpt-3.5-turbo`
**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 open>
<details>
<summary><strong>2023-02-24</strong></summary>
Version 2 is a major update that separates the backend functionality as an independent project, hosted at [chatgpt-ui-server](https://github.com/WongSaang/chatgpt-ui-server).
If you still wish to use the old version, please visit the [v1 branch](https://github.com/WongSaang/chatgpt-ui/tree/v1).
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.
- 😘 User authentication, supporting multiple users.
- 😀 Ability to store data in an external database (defaulting to Sqlite).
- 😎 Session persistence, allowing the API to answer questions based on your context.
</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
@@ -44,6 +90,9 @@ services:
image: wongsaang/chatgpt-ui-client:latest
environment:
- SERVER_DOMAIN=http://backend-web-server
- NUXT_PUBLIC_APP_NAME='ChatGPT UI' # App name
- NUXT_PUBLIC_TYPEWRITER=true # Enable typewriter effect, default is false
- NUXT_PUBLIC_TYPEWRITER_DELAY=100 # Typewriter effect delay time, default is 50ms
depends_on:
- backend-web-server
ports:
@@ -53,10 +102,18 @@ services:
backend-wsgi-server:
image: wongsaang/chatgpt-ui-wsgi-server:latest
environment:
# - 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.
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000} # CSRF whitelistAdd the address of your chatgpt-ui-web-server here, default is localhost:9000
#- DB_URL=postgres://postgres:postgrespw@localhost:49153/chatgpt # If this parameter is not set, the built-in Sqlite will be used by default. It should be noted that if you do not connect to an external database, the data will be lost after the container is destroyed.
#- OPENAI_API_PROXY=https://openai.proxy.com/v1 # Proxy for https://api.openai.com/v1
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name
- DJANGO_SUPERUSER_PASSWORD=password # default superuser password
- DJANGO_SUPERUSER_EMAIL=admin@example.com # default superuser email
# 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:
- '8000:8000'
networks:
@@ -77,16 +134,26 @@ networks:
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
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 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.
## Development

9
app.vue Normal file
View File

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

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

@@ -1,26 +1,83 @@
<script setup>
import { marked } from "marked"
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'
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 contentHtml = computed(() => {
return props.content ? marked(props.content) : ''
const contentHtml = ref('')
const contentElm = ref(null)
watchEffect(() => {
contentHtml.value = props.content ? md.render(props.content) : ''
})
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()
})
onUpdated(() => {
bindCopyCodeToButtons()
})
</script>
<template>
<div
ref="contentElm"
v-html="contentHtml"
class="text-body-1 text-justify"
class="chat-msg-content"
></div>
</template>
<style>
.chat-msg-content ol {
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>

View File

@@ -2,13 +2,12 @@
<v-textarea
v-model="message"
:label="$t('writeAMessage')"
:placeholder="$t('writeAMessage') + '...'"
:placeholder="hint"
rows="1"
:auto-grow="autoGrow"
:disabled="disabled"
:loading="loading"
:hint="hint"
:hide-details="loading"
:hide-details="true"
append-inner-icon="send"
@keyup.enter.exact="enterOnly"
@click:appendInner="clickSendBtn"
@@ -60,6 +59,9 @@ export default {
}
this.message = ""
},
usePrompt(prompt) {
this.message = prompt
},
clickSendBtn () {
this.send()
},

224
components/Prompt.vue Normal file
View File

@@ -0,0 +1,224 @@
<script setup>
const menu = ref(false)
const prompts = ref([])
const editingPrompt = 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({
prompt: newPrompt.value
})
})
if (!error.value) {
prompts.value.push(data.value)
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({
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
}
onMounted( () => {
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="speaker_notes"
title="Common prompts"
class="mr-3"
></v-btn>
</template>
<v-container>
<v-card
min-width="300"
max-width="500"
>
<v-card-title>
<span class="headline">Frequently prompts</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"
rounded="xl"
v-if="editingPrompt && editingPrompt.id === prompt.id"
>
<v-textarea
rows="2"
v-model="editingPrompt.prompt"
:loading="editingPrompt.updating"
variant="underlined"
hide-details
density="compact"
>
<template v-slot:append>
<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>
</template>
</v-textarea>
</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.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-textarea
rows="2"
v-model="newPrompt"
label="Add a new prompt"
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>
Add prompt
</v-btn>
</v-list-item>
</v-list>
</v-card>
</v-container>
</v-menu>
</div>
</template>
<style scoped>
</style>

BIN
demos/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 47 KiB

70
deployment.sh Normal file
View File

@@ -0,0 +1,70 @@
#!/bin/bash
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
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="${APP_DOMAIN}:${SERVER_PORT}" CLIENT_PORT=${CLIENT_PORT} SERVER_PORT=${SERVER_PORT} WSGI_PORT=${WSGI_PORT} docker-compose up --pull -d
echo "Done"

View File

@@ -4,15 +4,20 @@ services:
image: wongsaang/chatgpt-ui-client:latest
environment:
- SERVER_DOMAIN=http://backend-web-server
- NUXT_PUBLIC_APP_NAME='ChatGPT UI'
- NUXT_PUBLIC_TYPEWRITER=true
- NUXT_PUBLIC_TYPEWRITER_DELAY=100
depends_on:
- backend-web-server
ports:
- '80:80'
- '${CLIENT_PORT:-80}:80'
networks:
- chatgpt_ui_network
restart: always
backend-wsgi-server:
image: wongsaang/chatgpt-ui-wsgi-server:latest
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.
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name
- DJANGO_SUPERUSER_PASSWORD=password # default superuser password
@@ -24,19 +29,21 @@ services:
# - EMAIL_HOST_PASSWORD=
# - EMAIL_USE_TLS=True
ports:
- '8000:8000'
- '${WSGI_PORT:-8000}:8000'
networks:
- chatgpt_ui_network
restart: always
backend-web-server:
image: wongsaang/chatgpt-ui-web-server:latest
environment:
- BACKEND_URL=http://backend-wsgi-server:8000
ports:
- '9000:80'
- '${SERVER_PORT:-9000}:80'
depends_on:
- backend-wsgi-server
networks:
- chatgpt_ui_network
restart: always
networks:
chatgpt_ui_network:

181
docs/zh/README.md Normal file
View File

@@ -0,0 +1,181 @@
<p align="center">
<img alt="demo" src="../../demos/demo.gif?v=1">
</p>
[English](../../README.md) | [中文](./docs/zh/README.md)
# ChatGPT UI
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>
<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 名称,默认为 ChatGPT UI
- NUXT_PUBLIC_TYPEWRITER=true # 是否启用打字机效果,默认关闭
- NUXT_PUBLIC_TYPEWRITER_DELAY=100 # 打字机效果的延迟时间,默认 50毫秒
depends_on:
- backend-web-server
ports:
- '80:80'
networks:
- chatgpt_ui_network
backend-wsgi-server:
image: wongsaang/chatgpt-ui-wsgi-server:latest
environment:
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000} # CSRF 白名单,在这里设置为 chatgpt-ui-web-server 的地址+端口, 默认: localhost:9000
#- DB_URL=postgres://postgres:postgrespw@localhost:49153/chatgpt # 连接外部数据库,如果不设置这个参数,则默认使用内置的 Sqlite。需要注意的是如果不连接外部数据库数据将在容器销毁后丢失。链接格式请看下面的 DB_URL 格式对照表
#- OPENAI_API_PROXY=https://openai.proxy.com/v1 # https://api.openai.com/v1 的代理地址
- DJANGO_SUPERUSER_USERNAME=admin # 默认超级用户
- DJANGO_SUPERUSER_PASSWORD=password # 默认超级用户的密码
- DJANGO_SUPERUSER_EMAIL=admin@example.com # 默认超级用户邮箱
# 如果您想使用电子邮件验证功能,需要配置以下参数:
# - EMAIL_HOST=SMTP server address
# - EMAIL_PORT=SMTP server port
# - EMAIL_HOST_USER=
# - EMAIL_HOST_PASSWORD=
# - EMAIL_USE_TLS=True
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` 开始聊天。
## 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
```

View File

@@ -16,6 +16,15 @@
"followSystem": "Follow system",
"themeMode": "Theme Mode",
"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"

View File

@@ -16,6 +16,15 @@
"followSystem": "跟随系统",
"themeMode": "主题模式",
"feedback": "反馈",
"newConversation": "新的对话",
"clearConversations": "清除对话",
"modelParameters": "模型参数",
"model": "模型",
"temperature": "Temperature",
"topP": "Top P",
"frequencyPenalty": "Frequency Penalty",
"presencePenalty": "Presence Penalty",
"maxTokens": "Max Tokens",
"roles": {
"me": "我",
"ai": "AI"

View File

@@ -1,6 +1,5 @@
<script setup>
import {useConversions} from "../composables/states";
import {getConversions} from "../utils/helper";
import {useDisplay} from "vuetify";
const { $i18n } = useNuxtApp()
const runtimeConfig = useRuntimeConfig()
@@ -24,9 +23,73 @@ const setLang = (lang) => {
}
const conversations = useConversions()
const currentConversation = useConversion()
const editingConversation = ref(null)
const deletingConversationIndex = ref(null)
const editConversation = (index) => {
editingConversation.value = conversations.value[index]
}
const updateConversation = async (index) => {
editingConversation.value.updating = true
const { data, error } = await useAuthFetch(`/api/chat/conversations/${editingConversation.value.id}/`, {
method: 'PUT',
body: JSON.stringify({
topic: editingConversation.value.topic
})
})
if (!error.value) {
conversations.value[index] = editingConversation.value
}
editingConversation.value = null
}
const deleteConversation = async (index) => {
deletingConversationIndex.value = index
const { data, error } = await useAuthFetch(`/api/chat/conversations/${conversations.value[index].id}/`, {
method: 'DELETE'
})
deletingConversationIndex.value = null
if (!error.value) {
if (conversations.value[index].id === currentConversation.value.id) {
createNewConversion()
}
conversations.value.splice(index, 1)
}
}
const clearConversations = async () => {
deletingConversations.value = true
const { data, error } = await useAuthFetch(`/api/chat/conversations/delete_all`, {
method: 'DELETE'
})
if (!error.value) {
loadConversations()
clearConfirmDialog.value = false
}
deletingConversations.value = false
}
const clearConfirmDialog = ref(false)
const deletingConversations = ref(false)
const loadingConversations = ref(false)
const loadConversations = async () => {
loadingConversations.value = true
conversations.value = await getConversions()
loadingConversations.value = false
}
const {mdAndUp} = useDisplay()
const drawerPermanent = computed(() => {
return mdAndUp.value
})
onNuxtReady(async () => {
conversations.value = await getConversions()
loadConversations()
})
</script>
@@ -37,25 +100,86 @@ onNuxtReady(async () => {
>
<v-navigation-drawer
v-model="drawer"
:permanent="drawerPermanent"
width="300"
>
<div class="px-2 py-2">
<v-list>
<v-list-item>
<v-btn
block
variant="outlined"
prepend-icon="add"
size="large"
@click="createNewConversion()"
class="text-none"
>
New conversation
{{ $t('newConversation') }}
</v-btn>
</v-list-item>
<v-list-item v-show="loadingConversations">
<v-list-item-title class="d-flex justify-center">
<v-progress-circular indeterminate></v-progress-circular>
</v-list-item-title>
</v-list-item>
</v-list>
<v-list>
<v-list-item
v-for="conversation in conversations"
<template
v-for="(conversation, cIdx) in conversations"
:key="conversation.id"
:title="conversation.topic"
>
<v-list-item
active-color="primary"
rounded="xl"
v-if="editingConversation && editingConversation.id === conversation.id"
>
<v-text-field
v-model="editingConversation.topic"
:loading="editingConversation.updating"
variant="underlined"
append-icon="done"
hide-details
density="compact"
autofocus
@keyup.enter="updateConversation(cIdx)"
@click:append="updateConversation(cIdx)"
></v-text-field>
</v-list-item>
<v-hover
v-if="!editingConversation || editingConversation.id !== conversation.id"
v-slot="{ isHovering, props }"
>
<v-list-item
rounded="xl"
active-color="primary"
@click="openConversationMessages(conversation)"
></v-list-item>
v-bind="props"
>
<v-list-item-title>{{ conversation.topic }}</v-list-item-title>
<template v-slot:append>
<div
v-show="isHovering"
>
<v-btn
icon="edit"
size="small"
variant="text"
@click.stop="editConversation(cIdx)"
>
</v-btn>
<v-btn
icon="delete"
size="small"
variant="text"
:loading="deletingConversationIndex === cIdx"
@click.stop="deleteConversation(cIdx)"
>
</v-btn>
</div>
</template>
</v-list-item>
</v-hover>
</template>
</v-list>
</div>
@@ -64,6 +188,48 @@ onNuxtReady(async () => {
<v-divider></v-divider>
<v-list>
<v-dialog
v-model="clearConfirmDialog"
persistent
>
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
rounded="xl"
prepend-icon="delete_forever"
:title="$t('clearConversations')"
></v-list-item>
</template>
<v-card>
<v-card-title class="text-h5">
Are you sure you want to delete all conversations?
</v-card-title>
<v-card-text>This will be a permanent deletion and cannot be retrieved once deleted. Please proceed with caution.</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="green-darken-1"
variant="text"
@click="clearConfirmDialog = false"
class="text-none"
>
Cancel deletion
</v-btn>
<v-btn
color="green-darken-1"
variant="text"
@click="clearConversations"
class="text-none"
:loading="deletingConversations"
>
Confirm deletion
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<ModelParameters/>
<v-menu
>
<template v-slot:activator="{ props }">
@@ -101,7 +267,7 @@ onNuxtReady(async () => {
</v-navigation-drawer>
<v-app-bar
class="d-lg-none"
class="d-md-none"
>
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
@@ -109,29 +275,78 @@ onNuxtReady(async () => {
<v-spacer></v-spacer>
<v-menu
>
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
icon="help_outline"
title="Feedback"
:title="$t('newConversation')"
icon="add"
@click="createNewConversion()"
></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-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>
<div>
<div
v-if="$pwa?.offlineReady || $pwa?.needRefresh"
class="pwa-toast"
role="alert"
>
<div class="message">
<span v-if="$pwa.offlineReady">
App ready to work offline
</span>
<span v-else>
New content available, click on reload button to update.
</span>
</div>
<button
v-if="$pwa.needRefresh"
@click="$pwa.updateServiceWorker()"
>
Reload
</button>
<button @click="$pwa.cancelPrompt()">
Close
</button>
</div>
<div
v-if="$pwa?.showInstallPrompt && !$pwa?.offlineReady && !$pwa?.needRefresh"
class="pwa-toast"
role="alert"
>
<div class="message">
<span>
Install PWA
</span>
</div>
<button @click="$pwa.install()">
Install
</button>
<button @click="$pwa.cancelInstall()">
Cancel
</button>
</div>
</div>
</v-app>
</template>
@@ -146,4 +361,27 @@ onNuxtReady(async () => {
background-color: #999;
border-radius: 3px;
}
.pwa-toast {
position: fixed;
right: 0;
bottom: 0;
margin: 16px;
padding: 12px;
border: 1px solid #8885;
border-radius: 4px;
z-index: 1;
text-align: left;
box-shadow: 3px 4px 5px 0 #8885;
}
.pwa-toast .message {
margin-bottom: 8px;
}
.pwa-toast button {
border: 1px solid #8885;
outline: none;
margin-right: 5px;
border-radius: 2px;
padding: 3px 10px;
}
</style>

View File

@@ -11,7 +11,9 @@ export default defineNuxtConfig({
},
runtimeConfig: {
public: {
appName: appName
appName: appName,
typewriter: false,
typewriterDelay: 50,
}
},
build: {
@@ -23,9 +25,38 @@ export default defineNuxtConfig({
'highlight.js/styles/panda-syntax-dark.css',
],
modules: [
'@vite-pwa/nuxt',
'@nuxtjs/color-mode',
'@nuxtjs/i18n'
'@nuxtjs/i18n',
],
pwa: {
registerType: 'autoUpdate',
manifest: {
name: appName,
short_name: appName,
icons: [
{
src: 'icon-black.svg',
sizes: '900x900',
purpose: 'any maskable',
}
],
},
workbox: {
navigateFallback: '/',
globPatterns: ['**/*.{js,css,html,png,svg,ico}'],
},
client: {
installPrompt: true,
// you don't need to include this: only for testing purposes
// if enabling periodic sync for update use 1 hour or so (periodicSyncForUpdates: 3600)
periodicSyncForUpdates: 20,
},
devOptions: {
enabled: true,
type: 'module',
}
},
i18n: {
strategy: 'no_prefix',
locales: [

View File

@@ -15,9 +15,11 @@
},
"dependencies": {
"@microsoft/fetch-event-source": "^2.0.1",
"@vite-pwa/nuxt": "^0.0.7",
"copy-to-clipboard": "^3.3.3",
"highlight.js": "^11.7.0",
"is-mobile": "^3.1.1",
"marked": "^4.2.12",
"markdown-it": "^13.0.1",
"nanoid": "^4.0.1",
"vuetify": "^3.0.6"
},

View File

@@ -22,6 +22,7 @@
:rules="formRules.username"
label="User name"
variant="underlined"
clearable
></v-text-field>
<v-text-field
v-model="formData.password"
@@ -29,6 +30,10 @@
label="Password"
variant="underlined"
@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-form>
@@ -83,6 +88,7 @@ const signInForm = ref(null)
const valid = ref(true)
const submitting = ref(false)
const route = useRoute()
const passwordInputType = ref('password')
const submit = async () => {
errorMsg.value = null

View File

@@ -66,9 +66,13 @@ const submit = async () => {
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 {
$auth.setUser(data.value.user)
navigateTo('/account/onboarding')

View File

@@ -1,4 +1,6 @@
<script setup>
import Prompt from "~/components/Prompt.vue";
definePageMeta({
middleware: ["auth"]
})
@@ -10,6 +12,35 @@ const runtimeConfig = useRuntimeConfig()
const currentModel = useCurrentModel()
const openaiApiKey = useApiKey()
const fetchingResponse = ref(false)
const messageQueue = []
let isProcessingQueue = false
const processMessageQueue = () => {
if (isProcessingQueue || messageQueue.length === 0) {
return
}
if (!currentConversation.value.messages[currentConversation.value.messages.length - 1].is_bot) {
currentConversation.value.messages.push({id: null, is_bot: true, message: ''})
}
isProcessingQueue = true
const nextMessage = messageQueue.shift()
if (runtimeConfig.public.typewriter) {
let wordIndex = 0;
const intervalId = setInterval(() => {
currentConversation.value.messages[currentConversation.value.messages.length - 1].message += nextMessage[wordIndex]
wordIndex++
if (wordIndex === nextMessage.length) {
clearInterval(intervalId)
isProcessingQueue = false
processMessageQueue()
}
}, runtimeConfig.public.typewriterDelay)
} else {
currentConversation.value.messages[currentConversation.value.messages.length - 1].message += nextMessage
isProcessingQueue = false
processMessageQueue()
}
}
let ctrl
const abortFetch = () => {
@@ -20,6 +51,14 @@ const abortFetch = () => {
}
const fetchReply = async (message, parentMessageId) => {
ctrl = new AbortController()
const data = Object.assign({}, currentModel.value, {
openaiApiKey: openaiApiKey.value,
message: message,
parentMessageId: parentMessageId,
conversationId: currentConversation.value.id
})
try {
await fetchEventSource('/api/conversation/', {
signal: ctrl.signal,
@@ -28,13 +67,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;
@@ -69,11 +102,8 @@ const fetchReply = async (message, parentMessageId) => {
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})
}
messageQueue.push(data.content)
processMessageQueue()
scrollChatWindow()
},
@@ -120,6 +150,10 @@ const showSnackbar = (text) => {
snackbar.value = true
}
const editor = ref(null)
const usePrompt = (prompt) => {
editor.value.usePrompt(prompt)
}
</script>
@@ -168,6 +202,7 @@ const showSnackbar = (text) => {
<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"
@@ -175,7 +210,7 @@ const showSnackbar = (text) => {
class="mr-3"
@click="stop"
></v-btn>
<MsgEditor :send-message="send" :disabled="fetchingResponse" :loading="fetchingResponse" />
<MsgEditor ref="editor" :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">

View File

@@ -1,11 +1,13 @@
import { createVuetify } from 'vuetify'
import { aliases, md } from 'vuetify/iconsets/md'
import * as components from 'vuetify/components'
import { md3 } from 'vuetify/blueprints'
// import * as directives from 'vuetify/directives'
export default defineNuxtPlugin(nuxtApp => {
const vuetify = createVuetify({
ssr: true,
blueprint: md3,
icons: {
defaultSet: 'md',
aliases,

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

View File

@@ -1,6 +1,15 @@
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 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))
}
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)
let models = get(STORAGE_KEY.MODELS)
if (!models) {
models = [DEFAULT_OPENAI_MODEL]
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 = DEFAULT_MODEL
}
return model
}

2051
yarn.lock

File diff suppressed because it is too large Load Diff