Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
727826f1b1 | ||
|
|
386659109c | ||
|
|
bd9e8bf45e | ||
|
|
4e40530a8c | ||
|
|
ea69a350f4 | ||
|
|
18a4251714 | ||
|
|
878fda0054 | ||
|
|
1f3a025918 | ||
|
|
f9db3e5866 | ||
|
|
c9615ed05c | ||
|
|
0d4b6247e2 | ||
|
|
c9c3431cff | ||
|
|
46abf3daa0 | ||
|
|
8dcd7f46b1 | ||
|
|
33d9c392fa | ||
|
|
bb17cdd123 | ||
|
|
4cfc9f4aea | ||
|
|
cd50086c1e | ||
|
|
7e5498f779 | ||
|
|
d933236a5d | ||
|
|
0be2d45cd5 | ||
|
|
e24ad26d99 |
30
README.md
30
README.md
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="demo" src="./demos/demo.png?v=1">
|
<img alt="demo" src="./demos/demo.gif?v=1">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[English](./README.md) | [中文](./docs/zh/README.md)
|
[English](./README.md) | [中文](./docs/zh/README.md)
|
||||||
@@ -9,6 +9,23 @@
|
|||||||
A ChatGPT web client that supports multiple users, multiple database connections for persistent data storage, supports i18n. Provides Docker images and quick deployment scripts.
|
A ChatGPT web client that supports multiple users, multiple database connections for persistent data storage, supports i18n. Provides Docker images and quick deployment scripts.
|
||||||
|
|
||||||
## 📢Updates
|
## 📢Updates
|
||||||
|
<details open>
|
||||||
|
<summary><strong>2023-03-15</strong></summary>
|
||||||
|
|
||||||
|
Add "open_registration" setting option in the admin panel to control whether user registration is enabled. You can log in to the admin panel and find this setting option under `Chat->Setting`. The default value of this setting is `True` (allow user registration). If you do not need it, please change it to `False`.
|
||||||
|
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary><strong>2023-03-10</strong></summary>
|
||||||
|
|
||||||
|
Add 2 environment variables to control the typewriter effect:
|
||||||
|
|
||||||
|
- `NUXT_PUBLIC_TYPEWRITER=true` to enable/disable the typewriter effect
|
||||||
|
- `NUXT_PUBLIC_TYPEWRITER_DELAY=50` to set the delay time for each character in milliseconds.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
<details open>
|
<details open>
|
||||||
<summary><strong>2023-03-04</strong></summary>
|
<summary><strong>2023-03-04</strong></summary>
|
||||||
@@ -19,22 +36,22 @@ A ChatGPT web client that supports multiple users, multiple database connections
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details open>
|
<details>
|
||||||
|
|
||||||
<summary><strong>2023-02-24</strong></summary>
|
<summary><strong>2023-02-24</strong></summary>
|
||||||
Version 2 is a major update that separates the backend functionality as an independent project, hosted at [chatgpt-ui-server](https://github.com/WongSaang/chatgpt-ui-server).
|
Version 2 is a major update that separates the backend functionality as an independent project, hosted at [chatgpt-ui-server](https://github.com/WongSaang/chatgpt-ui-server).
|
||||||
|
|
||||||
If you still wish to use the old version, please visit the [v1 branch](https://github.com/WongSaang/chatgpt-ui/tree/v1).
|
If you still wish to use the old version, please visit the [v1 branch](https://github.com/WongSaang/chatgpt-ui/tree/v1).
|
||||||
|
|
||||||
Version 2 introduces the following new features:
|
</details>
|
||||||
|
|
||||||
|
## Version 2 introduces the following new features:
|
||||||
|
|
||||||
- 😉 Separation of the frontend and backend, with the backend now using the Python-based Django framework.
|
- 😉 Separation of the frontend and backend, with the backend now using the Python-based Django framework.
|
||||||
- 😘 User authentication, supporting multiple users.
|
- 😘 User authentication, supporting multiple users.
|
||||||
- 😀 Ability to store data in an external database (defaulting to Sqlite).
|
- 😀 Ability to store data in an external database (defaulting to Sqlite).
|
||||||
- 😎 Session persistence, allowing the API to answer questions based on your context.
|
- 😎 Session persistence, allowing the API to answer questions based on your context.
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## 🚀 One-click deployment <a name="one-click-depolyment"></a>
|
## 🚀 One-click deployment <a name="one-click-depolyment"></a>
|
||||||
|
|
||||||
Note: This script has only been tested on Ubuntu Server 22.04 LTS.
|
Note: This script has only been tested on Ubuntu Server 22.04 LTS.
|
||||||
@@ -73,6 +90,9 @@ services:
|
|||||||
image: wongsaang/chatgpt-ui-client:latest
|
image: wongsaang/chatgpt-ui-client:latest
|
||||||
environment:
|
environment:
|
||||||
- SERVER_DOMAIN=http://backend-web-server
|
- SERVER_DOMAIN=http://backend-web-server
|
||||||
|
- NUXT_PUBLIC_APP_NAME='ChatGPT UI' # App name
|
||||||
|
- NUXT_PUBLIC_TYPEWRITER=true # Enable typewriter effect, default is false
|
||||||
|
- NUXT_PUBLIC_TYPEWRITER_DELAY=100 # Typewriter effect delay time, default is 50ms
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend-web-server
|
- backend-web-server
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
9
app.vue
Normal file
9
app.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<VitePwaManifest />
|
||||||
|
<NuxtLoadingIndicator />
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
98
components/MessageActions.vue
Normal file
98
components/MessageActions.vue
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<script setup>
|
||||||
|
import copy from 'copy-to-clipboard'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
message: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
messageIndex: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
usePrompt: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
deleteMessage: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const snackbar = ref(false)
|
||||||
|
const snackbarText = ref('')
|
||||||
|
const showSnackbar = (text) => {
|
||||||
|
snackbarText.value = text
|
||||||
|
snackbar.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyMessage = () => {
|
||||||
|
copy(props.message.message)
|
||||||
|
showSnackbar('Copied!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const editMessage = () => {
|
||||||
|
props.usePrompt(props.message.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteMessage = async () => {
|
||||||
|
const { data, error } = await useAuthFetch(`/api/chat/messages/${props.message.id}/`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
if (!error.value) {
|
||||||
|
this.$emit('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>
|
||||||
191
components/ModelParameters.vue
Normal file
191
components/ModelParameters.vue
Normal 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>
|
||||||
@@ -1,34 +1,36 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { marked } from "marked"
|
|
||||||
import hljs from "highlight.js"
|
import hljs from "highlight.js"
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
|
|
||||||
// Part of the code comes from this project https://github.com/arronhunt/highlightjs-copy, thanks to the author's contribution
|
|
||||||
|
|
||||||
hljs.addPlugin({
|
const md = new MarkdownIt({
|
||||||
'after:highlightElement': ({ el, result, text }) => {
|
linkify: true,
|
||||||
let header = el.parentElement.querySelector(".hljs-code-header");
|
highlight(code, lang) {
|
||||||
if (header) {
|
const language = hljs.getLanguage(lang) ? lang : 'plaintext'
|
||||||
header.remove();
|
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>`
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps(['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) => {
|
||||||
header = Object.assign(document.createElement("div"), {
|
const copyButton = codeContainer.querySelector('.hljs-copy-button');
|
||||||
className: "hljs-code-header d-flex align-center justify-space-between bg-grey-darken-3 pa-1",
|
const codeBody = codeContainer.querySelector('code');
|
||||||
innerHTML: `<div class="pl-2 text-caption">${result.language}</div>`
|
|
||||||
});
|
|
||||||
|
|
||||||
let copyButton = Object.assign(document.createElement("button"), {
|
|
||||||
innerHTML: "Copy",
|
|
||||||
className: "hljs-copy-button",
|
|
||||||
});
|
|
||||||
copyButton.dataset.copied = false;
|
|
||||||
|
|
||||||
header.append(copyButton);
|
|
||||||
//
|
|
||||||
el.parentElement.classList.add("d-flex","flex-column", "my-3");
|
|
||||||
el.parentElement.prepend(header);
|
|
||||||
copyButton.onclick = function () {
|
copyButton.onclick = function () {
|
||||||
copy(text);
|
copy(codeBody.textContent ?? '');
|
||||||
|
|
||||||
copyButton.innerHTML = "Copied!";
|
copyButton.innerHTML = "Copied!";
|
||||||
copyButton.dataset.copied = 'true';
|
copyButton.dataset.copied = 'true';
|
||||||
@@ -38,39 +40,15 @@ hljs.addPlugin({
|
|||||||
copyButton.dataset.copied = 'false';
|
copyButton.dataset.copied = 'false';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
};
|
};
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
marked.setOptions({
|
|
||||||
// highlight: function (code, lang) {
|
|
||||||
// const language = hljs.getLanguage(lang) ? lang : 'plaintext'
|
|
||||||
// return hljs.highlight(code, { language }).value
|
|
||||||
// },
|
|
||||||
langPrefix: 'hljs language-', // highlight.js css class prefix
|
|
||||||
})
|
|
||||||
|
|
||||||
const props = defineProps(['content'])
|
|
||||||
|
|
||||||
const contentHtml = ref('')
|
|
||||||
|
|
||||||
const contentElm = ref(null)
|
|
||||||
|
|
||||||
const highlightCode = () => {
|
|
||||||
if (!contentElm.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
contentElm.value.querySelectorAll('pre code').forEach((block) => {
|
|
||||||
hljs.highlightElement(block)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
watchEffect(() => {
|
onMounted(() => {
|
||||||
contentHtml.value = props.content ? marked(props.content) : ''
|
bindCopyCodeToButtons()
|
||||||
if (props.content && props.content.endsWith('```')) {
|
|
||||||
nextTick(() => {
|
|
||||||
highlightCode()
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
onUpdated(() => {
|
||||||
|
bindCopyCodeToButtons()
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -79,13 +57,17 @@ watchEffect(() => {
|
|||||||
<div
|
<div
|
||||||
ref="contentElm"
|
ref="contentElm"
|
||||||
v-html="contentHtml"
|
v-html="contentHtml"
|
||||||
class="chat-msg-content text-justify"
|
class="chat-msg-content"
|
||||||
></div>
|
></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.chat-msg-content ol {
|
.chat-msg-content ol {
|
||||||
list-style-position: inside;
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
.hljs-code-container {
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.hljs-copy-button{
|
.hljs-copy-button{
|
||||||
width:2rem;height:2rem;text-indent:-9999px;color:#fff;
|
width:2rem;height:2rem;text-indent:-9999px;color:#fff;
|
||||||
|
|||||||
@@ -1,17 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex-grow-1 d-flex align-center justify-space-between"
|
||||||
|
>
|
||||||
<v-textarea
|
<v-textarea
|
||||||
v-model="message"
|
v-model="message"
|
||||||
:label="$t('writeAMessage')"
|
:label="$t('writeAMessage')"
|
||||||
:placeholder="hint"
|
:placeholder="hint"
|
||||||
rows="1"
|
:rows="rows"
|
||||||
|
max-rows="8"
|
||||||
:auto-grow="autoGrow"
|
:auto-grow="autoGrow"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:hide-details="true"
|
:hide-details="true"
|
||||||
append-inner-icon="send"
|
clearable
|
||||||
@keyup.enter.exact="enterOnly"
|
variant="outlined"
|
||||||
@click:appendInner="clickSendBtn"
|
@keydown.enter.exact="enterOnly"
|
||||||
></v-textarea>
|
></v-textarea>
|
||||||
|
<v-btn
|
||||||
|
:disabled="loading"
|
||||||
|
icon="send"
|
||||||
|
title="Send"
|
||||||
|
class="ml-3"
|
||||||
|
@click="clickSendBtn"
|
||||||
|
></v-btn>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -39,7 +51,7 @@ export default {
|
|||||||
message(val) {
|
message(val) {
|
||||||
const lines = val.split(/\r\n|\r|\n/).length;
|
const lines = val.split(/\r\n|\r|\n/).length;
|
||||||
if (lines > 8) {
|
if (lines > 8) {
|
||||||
this.rows = lines;
|
this.rows = 8;
|
||||||
this.autoGrow = false;
|
this.autoGrow = false;
|
||||||
} else {
|
} else {
|
||||||
this.rows = 1;
|
this.rows = 1;
|
||||||
@@ -65,7 +77,8 @@ export default {
|
|||||||
clickSendBtn () {
|
clickSendBtn () {
|
||||||
this.send()
|
this.send()
|
||||||
},
|
},
|
||||||
enterOnly () {
|
enterOnly (event) {
|
||||||
|
event.preventDefault();
|
||||||
if (!isMobile()) {
|
if (!isMobile()) {
|
||||||
this.send()
|
this.send()
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
demos/demo.gif
Normal file
BIN
demos/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
@@ -1,6 +1,28 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
read -p "Please enter a resolved domain name: " domain
|
read -p "Please enter a domain name or external IP address [default: localhost]: " APP_DOMAIN
|
||||||
|
|
||||||
|
if [ -z "$APP_DOMAIN" ]; then
|
||||||
|
APP_DOMAIN="localhost"
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -p "Please set a port for the frontend server [default: 80]: " CLIENT_PORT
|
||||||
|
|
||||||
|
if [ -z "$CLIENT_PORT" ]; then
|
||||||
|
CLIENT_PORT="80"
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -p "Please set a port for the backend server [default: 9000]: " SERVER_PORT
|
||||||
|
|
||||||
|
if [ -z "$SERVER_PORT" ]; then
|
||||||
|
SERVER_PORT="9000"
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -p "Please set a port for the backend WSGI server [default: 8000]: " WSGI_PORT
|
||||||
|
|
||||||
|
if [ -z "$WSGI_PORT" ]; then
|
||||||
|
WSGI_PORT="8000"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ $(which docker) ]]; then
|
if [[ $(which docker) ]]; then
|
||||||
echo "Docker is already installed"
|
echo "Docker is already installed"
|
||||||
@@ -43,6 +65,6 @@ sudo curl -L "https://raw.githubusercontent.com/WongSaang/chatgpt-ui/main/docker
|
|||||||
|
|
||||||
echo "Starting services..."
|
echo "Starting services..."
|
||||||
|
|
||||||
sudo APP_DOMAIN="${domain}:9000" docker-compose up -d
|
sudo APP_DOMAIN="${APP_DOMAIN}:${SERVER_PORT}" CLIENT_PORT=${CLIENT_PORT} SERVER_PORT=${SERVER_PORT} WSGI_PORT=${WSGI_PORT} docker-compose up --pull -d
|
||||||
|
|
||||||
echo "Done"
|
echo "Done"
|
||||||
@@ -4,12 +4,16 @@ services:
|
|||||||
image: wongsaang/chatgpt-ui-client:latest
|
image: wongsaang/chatgpt-ui-client:latest
|
||||||
environment:
|
environment:
|
||||||
- SERVER_DOMAIN=http://backend-web-server
|
- SERVER_DOMAIN=http://backend-web-server
|
||||||
|
- NUXT_PUBLIC_APP_NAME='ChatGPT UI'
|
||||||
|
- NUXT_PUBLIC_TYPEWRITER=true
|
||||||
|
- NUXT_PUBLIC_TYPEWRITER_DELAY=100
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend-web-server
|
- backend-web-server
|
||||||
ports:
|
ports:
|
||||||
- '80:80'
|
- '${CLIENT_PORT:-80}:80'
|
||||||
networks:
|
networks:
|
||||||
- chatgpt_ui_network
|
- chatgpt_ui_network
|
||||||
|
restart: always
|
||||||
backend-wsgi-server:
|
backend-wsgi-server:
|
||||||
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||||
environment:
|
environment:
|
||||||
@@ -25,19 +29,21 @@ services:
|
|||||||
# - EMAIL_HOST_PASSWORD=
|
# - EMAIL_HOST_PASSWORD=
|
||||||
# - EMAIL_USE_TLS=True
|
# - EMAIL_USE_TLS=True
|
||||||
ports:
|
ports:
|
||||||
- '8000:8000'
|
- '${WSGI_PORT:-8000}:8000'
|
||||||
networks:
|
networks:
|
||||||
- chatgpt_ui_network
|
- chatgpt_ui_network
|
||||||
|
restart: always
|
||||||
backend-web-server:
|
backend-web-server:
|
||||||
image: wongsaang/chatgpt-ui-web-server:latest
|
image: wongsaang/chatgpt-ui-web-server:latest
|
||||||
environment:
|
environment:
|
||||||
- BACKEND_URL=http://backend-wsgi-server:8000
|
- BACKEND_URL=http://backend-wsgi-server:8000
|
||||||
ports:
|
ports:
|
||||||
- '9000:80'
|
- '${SERVER_PORT:-9000}:80'
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend-wsgi-server
|
- backend-wsgi-server
|
||||||
networks:
|
networks:
|
||||||
- chatgpt_ui_network
|
- chatgpt_ui_network
|
||||||
|
restart: always
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
chatgpt_ui_network:
|
chatgpt_ui_network:
|
||||||
|
|||||||
@@ -1,14 +1,30 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="demo" src="./demos/demo.png?v=1">
|
<img alt="demo" src="../../demos/demo.gif?v=1">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[English](./README.md) | [中文](./docs/zh/README.md)
|
[English](../../README.md) | [中文](./docs/zh/README.md)
|
||||||
|
|
||||||
# ChatGPT UI
|
# ChatGPT UI
|
||||||
|
|
||||||
ChatGPT Web 客户端,支持多用户,支持 Mysql、PostgreSQL 等多种数据库连接进行数据持久化存储,支持多语言。提供 Docker 镜像和快速部署脚本。
|
ChatGPT Web 客户端,支持多用户,支持 Mysql、PostgreSQL 等多种数据库连接进行数据持久化存储,支持多语言。提供 Docker 镜像和快速部署脚本。
|
||||||
|
|
||||||
## 📢 更新
|
## 📢 更新
|
||||||
|
<details open>
|
||||||
|
<summary><strong>2023-03-15</strong></summary>
|
||||||
|
|
||||||
|
在管理后台增加 `open_registration` 设置项,用于控制是否开放用户注册。你可以登录管理后台,在 `Chat->Setting` 中看到这个设置项,默认是 `True` (允许用户注册),如果不需要,请改成 `False`。
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary><strong>2023-03-10</strong></summary>
|
||||||
|
|
||||||
|
增加 2 个环境变量来控制打字机效果, 详见下方 docker-compose 配置的环境变量说明
|
||||||
|
|
||||||
|
- `NUXT_PUBLIC_TYPEWRITER` 是否开启打字机效果
|
||||||
|
- `NUXT_PUBLIC_TYPEWRITER_DELAY` 每个字的延迟时间,单位:毫秒
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
<details open>
|
<details open>
|
||||||
<summary><strong>2023-03-04</strong></summary>
|
<summary><strong>2023-03-04</strong></summary>
|
||||||
@@ -19,7 +35,7 @@ ChatGPT Web 客户端,支持多用户,支持 Mysql、PostgreSQL 等多种数
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details open>
|
<details>
|
||||||
|
|
||||||
<summary><strong>2023-02-24</strong></summary>
|
<summary><strong>2023-02-24</strong></summary>
|
||||||
V2 是一个重要的更新,将后端功能分离为一个独立的项目,托管在 [chatgpt-ui-server](https://github.com/WongSaang/chatgpt-ui-server), 该项目使用基于 Python 的 Django 框架。
|
V2 是一个重要的更新,将后端功能分离为一个独立的项目,托管在 [chatgpt-ui-server](https://github.com/WongSaang/chatgpt-ui-server), 该项目使用基于 Python 的 Django 框架。
|
||||||
@@ -72,6 +88,9 @@ services:
|
|||||||
image: wongsaang/chatgpt-ui-client:latest
|
image: wongsaang/chatgpt-ui-client:latest
|
||||||
environment:
|
environment:
|
||||||
- SERVER_DOMAIN=http://backend-web-server
|
- SERVER_DOMAIN=http://backend-web-server
|
||||||
|
- NUXT_PUBLIC_APP_NAME='ChatGPT UI' # App 名称,默认为 ChatGPT UI
|
||||||
|
- NUXT_PUBLIC_TYPEWRITER=true # 是否启用打字机效果,默认关闭
|
||||||
|
- NUXT_PUBLIC_TYPEWRITER_DELAY=100 # 打字机效果的延迟时间,默认 50毫秒
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend-web-server
|
- backend-web-server
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -18,10 +18,22 @@
|
|||||||
"feedback": "Feedback",
|
"feedback": "Feedback",
|
||||||
"newConversation": "New conversation",
|
"newConversation": "New conversation",
|
||||||
"clearConversations": "Clear conversations",
|
"clearConversations": "Clear conversations",
|
||||||
|
"modelParameters": "Model Parameters",
|
||||||
|
"model": "Model",
|
||||||
|
"temperature": "Temperature",
|
||||||
|
"topP": "Top P",
|
||||||
|
"frequencyPenalty": "Frequency Penalty",
|
||||||
|
"presencePenalty": "Presence Penalty",
|
||||||
|
"maxTokens": "Max Tokens",
|
||||||
"roles": {
|
"roles": {
|
||||||
"me": "Me",
|
"me": "Me",
|
||||||
"ai": "AI"
|
"ai": "AI"
|
||||||
},
|
},
|
||||||
|
"edit": "Edit",
|
||||||
|
"copy": "Copy",
|
||||||
|
"copied": "Copied",
|
||||||
|
"delete": "Delete",
|
||||||
|
"signOut": "Sign out",
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"introduction1": "is an unofficial client for ChatGPT, but uses the official OpenAI API.",
|
"introduction1": "is an unofficial client for ChatGPT, but uses the official OpenAI API.",
|
||||||
"introduction2": "You will need an OpenAI API Key before you can use this client.",
|
"introduction2": "You will need an OpenAI API Key before you can use this client.",
|
||||||
|
|||||||
@@ -18,10 +18,22 @@
|
|||||||
"feedback": "反馈",
|
"feedback": "反馈",
|
||||||
"newConversation": "新的对话",
|
"newConversation": "新的对话",
|
||||||
"clearConversations": "清除对话",
|
"clearConversations": "清除对话",
|
||||||
|
"modelParameters": "模型参数",
|
||||||
|
"model": "模型",
|
||||||
|
"temperature": "Temperature",
|
||||||
|
"topP": "Top P",
|
||||||
|
"frequencyPenalty": "Frequency Penalty",
|
||||||
|
"presencePenalty": "Presence Penalty",
|
||||||
|
"maxTokens": "Max Tokens",
|
||||||
"roles": {
|
"roles": {
|
||||||
"me": "我",
|
"me": "我",
|
||||||
"ai": "AI"
|
"ai": "AI"
|
||||||
},
|
},
|
||||||
|
"edit": "编辑",
|
||||||
|
"copy": "复制",
|
||||||
|
"copied": "已复制",
|
||||||
|
"delete": "删除",
|
||||||
|
"signOut": "退出登录",
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"introduction1": "是一个非官方的ChatGPT客户端,但使用OpenAI的官方API",
|
"introduction1": "是一个非官方的ChatGPT客户端,但使用OpenAI的官方API",
|
||||||
"introduction2": "在使用本客户端之前,您需要一个OpenAI API密钥。",
|
"introduction2": "在使用本客户端之前,您需要一个OpenAI API密钥。",
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {useConversions} from "../composables/states";
|
|
||||||
import {getConversions} from "../utils/helper";
|
|
||||||
import {useDisplay} from "vuetify";
|
import {useDisplay} from "vuetify";
|
||||||
|
|
||||||
const { $i18n } = useNuxtApp()
|
const { $i18n, $auth } = useNuxtApp()
|
||||||
const runtimeConfig = useRuntimeConfig()
|
const runtimeConfig = useRuntimeConfig()
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
const drawer = ref(null)
|
const drawer = ref(null)
|
||||||
@@ -25,6 +23,7 @@ const setLang = (lang) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const conversations = useConversions()
|
const conversations = useConversions()
|
||||||
|
const currentConversation = useConversion()
|
||||||
|
|
||||||
const editingConversation = ref(null)
|
const editingConversation = ref(null)
|
||||||
const deletingConversationIndex = ref(null)
|
const deletingConversationIndex = ref(null)
|
||||||
@@ -54,6 +53,9 @@ const deleteConversation = async (index) => {
|
|||||||
})
|
})
|
||||||
deletingConversationIndex.value = null
|
deletingConversationIndex.value = null
|
||||||
if (!error.value) {
|
if (!error.value) {
|
||||||
|
if (conversations.value[index].id === currentConversation.value.id) {
|
||||||
|
createNewConversion()
|
||||||
|
}
|
||||||
conversations.value.splice(index, 1)
|
conversations.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,6 +88,15 @@ const drawerPermanent = computed(() => {
|
|||||||
return mdAndUp.value
|
return mdAndUp.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const signOut = async () => {
|
||||||
|
const { data, error } = await useFetch('/api/account/logout/', {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
if (!error.value) {
|
||||||
|
await $auth.logout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onNuxtReady(async () => {
|
onNuxtReady(async () => {
|
||||||
loadConversations()
|
loadConversations()
|
||||||
})
|
})
|
||||||
@@ -162,7 +173,7 @@ onNuxtReady(async () => {
|
|||||||
icon="edit"
|
icon="edit"
|
||||||
size="small"
|
size="small"
|
||||||
variant="text"
|
variant="text"
|
||||||
@click="editConversation(cIdx)"
|
@click.stop="editConversation(cIdx)"
|
||||||
>
|
>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
@@ -170,7 +181,7 @@ onNuxtReady(async () => {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="text"
|
variant="text"
|
||||||
:loading="deletingConversationIndex === cIdx"
|
:loading="deletingConversationIndex === cIdx"
|
||||||
@click="deleteConversation(cIdx)"
|
@click.stop="deleteConversation(cIdx)"
|
||||||
>
|
>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -226,6 +237,8 @@ onNuxtReady(async () => {
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
|
<ModelParameters/>
|
||||||
|
|
||||||
<v-menu
|
<v-menu
|
||||||
>
|
>
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
@@ -257,6 +270,14 @@ onNuxtReady(async () => {
|
|||||||
:title="$t('feedback')"
|
:title="$t('feedback')"
|
||||||
@click="feedback"
|
@click="feedback"
|
||||||
></v-list-item>
|
></v-list-item>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
rounded="xl"
|
||||||
|
prepend-icon="logout"
|
||||||
|
:title="$t('signOut')"
|
||||||
|
@click="signOut"
|
||||||
|
></v-list-item>
|
||||||
|
|
||||||
</v-list>
|
</v-list>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -277,29 +298,54 @@ onNuxtReady(async () => {
|
|||||||
@click="createNewConversion()"
|
@click="createNewConversion()"
|
||||||
></v-btn>
|
></v-btn>
|
||||||
|
|
||||||
<!-- <v-menu-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <template v-slot:activator="{ props }">-->
|
|
||||||
<!-- <v-btn-->
|
|
||||||
<!-- v-bind="props"-->
|
|
||||||
<!-- icon="help_outline"-->
|
|
||||||
<!-- title="Feedback"-->
|
|
||||||
<!-- ></v-btn>-->
|
|
||||||
<!-- </template>-->
|
|
||||||
<!-- <v-list-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <v-list-item-->
|
|
||||||
<!-- @click="feedback"-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <v-list-item-title>{{ $t('feedback') }}</v-list-item-title>-->
|
|
||||||
<!-- </v-list-item>-->
|
|
||||||
<!-- </v-list>-->
|
|
||||||
<!-- </v-menu>-->
|
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
|
|
||||||
<v-main>
|
<v-main>
|
||||||
<NuxtPage/>
|
<NuxtPage/>
|
||||||
</v-main>
|
</v-main>
|
||||||
|
|
||||||
|
<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>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -314,4 +360,27 @@ onNuxtReady(async () => {
|
|||||||
background-color: #999;
|
background-color: #999;
|
||||||
border-radius: 3px;
|
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>
|
</style>
|
||||||
@@ -11,7 +11,9 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
appName: appName
|
appName: appName,
|
||||||
|
typewriter: false,
|
||||||
|
typewriterDelay: 50,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
@@ -23,9 +25,38 @@ export default defineNuxtConfig({
|
|||||||
'highlight.js/styles/panda-syntax-dark.css',
|
'highlight.js/styles/panda-syntax-dark.css',
|
||||||
],
|
],
|
||||||
modules: [
|
modules: [
|
||||||
|
'@vite-pwa/nuxt',
|
||||||
'@nuxtjs/color-mode',
|
'@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: false,
|
||||||
|
type: 'module',
|
||||||
|
}
|
||||||
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
strategy: 'no_prefix',
|
strategy: 'no_prefix',
|
||||||
locales: [
|
locales: [
|
||||||
@@ -52,7 +83,7 @@ export default defineNuxtConfig({
|
|||||||
nitro: {
|
nitro: {
|
||||||
devProxy: {
|
devProxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://localhost:8000/api",
|
target: process.env.NUXT_DEV_SERVER ?? 'http://localhost:8000/api',
|
||||||
prependPath: true,
|
prependPath: true,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@microsoft/fetch-event-source": "^2.0.1",
|
"@microsoft/fetch-event-source": "^2.0.1",
|
||||||
|
"@vite-pwa/nuxt": "^0.0.7",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
"highlight.js": "^11.7.0",
|
"highlight.js": "^11.7.0",
|
||||||
"is-mobile": "^3.1.1",
|
"is-mobile": "^3.1.1",
|
||||||
"marked": "^4.2.12",
|
"markdown-it": "^13.0.1",
|
||||||
"nanoid": "^4.0.1",
|
"nanoid": "^4.0.1",
|
||||||
"vuetify": "^3.0.6"
|
"vuetify": "^3.0.6"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -66,9 +66,13 @@ const submit = async () => {
|
|||||||
if (error.value.data.non_field_errors) {
|
if (error.value.data.non_field_errors) {
|
||||||
errorMsg.value = error.value.data.non_field_errors[0]
|
errorMsg.value = error.value.data.non_field_errors[0]
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (error.value.data.detail) {
|
||||||
|
errorMsg.value = error.value.data.detail
|
||||||
} else {
|
} else {
|
||||||
errorMsg.value = 'Something went wrong. Please try again.'
|
errorMsg.value = 'Something went wrong. Please try again.'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$auth.setUser(data.value.user)
|
$auth.setUser(data.value.user)
|
||||||
navigateTo('/account/onboarding')
|
navigateTo('/account/onboarding')
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ definePageMeta({
|
|||||||
})
|
})
|
||||||
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
|
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
|
||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
|
import MessageActions from "~/components/MessageActions.vue";
|
||||||
|
|
||||||
const { $i18n, $auth } = useNuxtApp()
|
const { $i18n, $auth } = useNuxtApp()
|
||||||
const runtimeConfig = useRuntimeConfig()
|
const runtimeConfig = useRuntimeConfig()
|
||||||
@@ -24,6 +25,7 @@ const processMessageQueue = () => {
|
|||||||
}
|
}
|
||||||
isProcessingQueue = true
|
isProcessingQueue = true
|
||||||
const nextMessage = messageQueue.shift()
|
const nextMessage = messageQueue.shift()
|
||||||
|
if (runtimeConfig.public.typewriter) {
|
||||||
let wordIndex = 0;
|
let wordIndex = 0;
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
currentConversation.value.messages[currentConversation.value.messages.length - 1].message += nextMessage[wordIndex]
|
currentConversation.value.messages[currentConversation.value.messages.length - 1].message += nextMessage[wordIndex]
|
||||||
@@ -33,7 +35,12 @@ const processMessageQueue = () => {
|
|||||||
isProcessingQueue = false
|
isProcessingQueue = false
|
||||||
processMessageQueue()
|
processMessageQueue()
|
||||||
}
|
}
|
||||||
}, 50)
|
}, runtimeConfig.public.typewriterDelay)
|
||||||
|
} else {
|
||||||
|
currentConversation.value.messages[currentConversation.value.messages.length - 1].message += nextMessage
|
||||||
|
isProcessingQueue = false
|
||||||
|
processMessageQueue()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let ctrl
|
let ctrl
|
||||||
@@ -45,6 +52,14 @@ const abortFetch = () => {
|
|||||||
}
|
}
|
||||||
const fetchReply = async (message, parentMessageId) => {
|
const fetchReply = async (message, parentMessageId) => {
|
||||||
ctrl = new AbortController()
|
ctrl = new AbortController()
|
||||||
|
|
||||||
|
const data = Object.assign({}, currentModel.value, {
|
||||||
|
openaiApiKey: openaiApiKey.value,
|
||||||
|
message: message,
|
||||||
|
parentMessageId: parentMessageId,
|
||||||
|
conversationId: currentConversation.value.id
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetchEventSource('/api/conversation/', {
|
await fetchEventSource('/api/conversation/', {
|
||||||
signal: ctrl.signal,
|
signal: ctrl.signal,
|
||||||
@@ -53,13 +68,7 @@ const fetchReply = async (message, parentMessageId) => {
|
|||||||
'accept': 'application/json',
|
'accept': 'application/json',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(data),
|
||||||
model: currentModel.value,
|
|
||||||
openaiApiKey: openaiApiKey.value,
|
|
||||||
message: message,
|
|
||||||
parentMessageId: parentMessageId,
|
|
||||||
conversationId: currentConversation.value.id
|
|
||||||
}),
|
|
||||||
onopen(response) {
|
onopen(response) {
|
||||||
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
|
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
|
||||||
return;
|
return;
|
||||||
@@ -147,6 +156,10 @@ const usePrompt = (prompt) => {
|
|||||||
editor.value.usePrompt(prompt)
|
editor.value.usePrompt(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteMessage = (index) => {
|
||||||
|
currentConversation.value.messages.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -161,9 +174,16 @@ const usePrompt = (prompt) => {
|
|||||||
cols="12"
|
cols="12"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="d-flex"
|
class="d-flex align-center"
|
||||||
:class="message.is_bot ? 'justify-start mr-16' : 'justify-end ml-16'"
|
:class="message.is_bot ? 'justify-start' : 'justify-end'"
|
||||||
>
|
>
|
||||||
|
<MessageActions
|
||||||
|
v-if="!message.is_bot"
|
||||||
|
:message="message"
|
||||||
|
:message-index="index"
|
||||||
|
:use-prompt="usePrompt"
|
||||||
|
:delete-message="deleteMessage"
|
||||||
|
/>
|
||||||
<v-card
|
<v-card
|
||||||
:color="message.is_bot ? '' : 'primary'"
|
:color="message.is_bot ? '' : 'primary'"
|
||||||
rounded="lg"
|
rounded="lg"
|
||||||
@@ -172,18 +192,14 @@ const usePrompt = (prompt) => {
|
|||||||
<v-card-text>
|
<v-card-text>
|
||||||
<MsgContent :content="message.message" />
|
<MsgContent :content="message.message" />
|
||||||
</v-card-text>
|
</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>
|
</v-card>
|
||||||
|
<MessageActions
|
||||||
|
v-if="message.is_bot"
|
||||||
|
:message="message"
|
||||||
|
:message-index="index"
|
||||||
|
:use-prompt="usePrompt"
|
||||||
|
:delete-message="deleteMessage"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|||||||
3
public/icon-black.svg
Normal file
3
public/icon-black.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="900" height="900" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M504.908 750H839.476C850.103 750.001 860.542 747.229 869.745 741.963C878.948 736.696 886.589 729.121 891.9 719.999C897.211 710.876 900.005 700.529 900 689.997C899.995 679.465 897.193 669.12 891.873 660.002L667.187 274.289C661.876 265.169 654.237 257.595 645.036 252.329C635.835 247.064 625.398 244.291 614.773 244.291C604.149 244.291 593.711 247.064 584.511 252.329C575.31 257.595 567.67 265.169 562.36 274.289L504.908 372.979L392.581 179.993C387.266 170.874 379.623 163.301 370.42 158.036C361.216 152.772 350.777 150 340.151 150C329.525 150 319.086 152.772 309.883 158.036C300.679 163.301 293.036 170.874 287.721 179.993L8.12649 660.002C2.80743 669.12 0.00462935 679.465 5.72978e-06 689.997C-0.00461789 700.529 2.78909 710.876 8.10015 719.999C13.4112 729.121 21.0523 736.696 30.255 741.963C39.4576 747.229 49.8973 750.001 60.524 750H270.538C353.748 750 415.112 713.775 457.336 643.101L559.849 467.145L614.757 372.979L779.547 655.834H559.849L504.908 750ZM267.114 655.737L120.551 655.704L340.249 278.586L449.87 467.145L376.474 593.175C348.433 639.03 316.577 655.737 267.114 655.737Z" fill="#0C0C0D"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,6 +1,15 @@
|
|||||||
|
|
||||||
export const STORAGE_KEY = {
|
export const STORAGE_KEY = {
|
||||||
OPENAI_MODELS: 'openai_models',
|
MODELS: 'models',
|
||||||
CURRENT_OPENAI_MODEL: 'current_openai_model',
|
CURRENT_MODEL: 'current_model',
|
||||||
OPENAI_API_KEY: 'openai_api_key',
|
OPENAI_API_KEY: 'openai_api_key',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_MODEL = {
|
||||||
|
name: 'gpt-3.5-turbo',
|
||||||
|
frequency_penalty: 0.0,
|
||||||
|
presence_penalty: 0.0,
|
||||||
|
max_tokens: 1000,
|
||||||
|
temperature: 0.7,
|
||||||
|
top_p: 1.0
|
||||||
|
}
|
||||||
@@ -11,32 +11,28 @@ const set = (key, val) => {
|
|||||||
localStorage.setItem(key, JSON.stringify(val))
|
localStorage.setItem(key, JSON.stringify(val))
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_OPENAI_MODEL = 'text-davinci-003'
|
|
||||||
|
|
||||||
export const setModels = (val) => {
|
export const setModels = (val) => {
|
||||||
const models = useModels()
|
const models = useModels()
|
||||||
set(STORAGE_KEY.OPENAI_MODELS, val)
|
set(STORAGE_KEY.MODELS, val)
|
||||||
models.value = val
|
models.value = val
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStoredModels = () => {
|
export const getStoredModels = () => {
|
||||||
let models = get(STORAGE_KEY.OPENAI_MODELS)
|
let models = get(STORAGE_KEY.MODELS)
|
||||||
if (!models) {
|
if (!models) {
|
||||||
models = [DEFAULT_OPENAI_MODEL]
|
models = [DEFAULT_MODEL]
|
||||||
}
|
}
|
||||||
return models
|
return models
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setCurrentModel = (val) => {
|
export const saveCurrentModel = (val) => {
|
||||||
const model = useCurrentModel()
|
set(STORAGE_KEY.CURRENT_MODEL, val)
|
||||||
set(STORAGE_KEY.CURRENT_OPENAI_MODEL, val)
|
|
||||||
model.value = val
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCurrentModel = () => {
|
export const getCurrentModel = () => {
|
||||||
let model = get(STORAGE_KEY.CURRENT_OPENAI_MODEL)
|
let model = get(STORAGE_KEY.CURRENT_MODEL)
|
||||||
if (!model) {
|
if (!model) {
|
||||||
model = DEFAULT_OPENAI_MODEL
|
model = DEFAULT_MODEL
|
||||||
}
|
}
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user