Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
deb627a9ab | ||
|
|
70efc09dae | ||
|
|
8ff914582a | ||
|
|
f20a3562f3 | ||
|
|
4a1adf6d00 | ||
|
|
ddce1c9721 |
@@ -105,6 +105,7 @@ services:
|
||||
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||
environment:
|
||||
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000} # CSRF whitelist,Add the address of your chatgpt-ui-web-server here, default is localhost:9000
|
||||
- SERVER_WORKERS=3 # Number of gunicorn workers, default is 3
|
||||
#- 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
|
||||
|
||||
@@ -64,18 +64,16 @@ onUpdated(() => {
|
||||
rounded="lg"
|
||||
elevation="2"
|
||||
>
|
||||
<v-card-text>
|
||||
<div
|
||||
ref="contentElm"
|
||||
v-html="contentHtml"
|
||||
class="chat-msg-content"
|
||||
></div>
|
||||
</v-card-text>
|
||||
<div
|
||||
ref="contentElm"
|
||||
v-html="contentHtml"
|
||||
class="chat-msg-content pa-3"
|
||||
></div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.chat-msg-content ol {
|
||||
.chat-msg-content ol, .chat-msg-content ul {
|
||||
padding-left: 2em;
|
||||
}
|
||||
.hljs-code-container {
|
||||
|
||||
@@ -1,3 +1,73 @@
|
||||
<script setup>
|
||||
import { isMobile } from 'is-mobile'
|
||||
const { $i18n } = useNuxtApp()
|
||||
|
||||
const props = defineProps({
|
||||
sendMessage: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const message = ref('')
|
||||
const rows = ref(1)
|
||||
const autoGrow = ref(true)
|
||||
|
||||
const hint = computed(() => {
|
||||
return isMobile() ? '' : $i18n.t('pressEnterToSendYourMessageOrShiftEnterToAddANewLine')
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const lines = message.value.split(/\r\n|\r|\n/).length
|
||||
if (lines > 8) {
|
||||
rows.value = 8
|
||||
autoGrow.value = false
|
||||
} else {
|
||||
rows.value = 1
|
||||
autoGrow.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const send = () => {
|
||||
let msg = message.value
|
||||
// remove the last "\n"
|
||||
if (msg[msg.length - 1] === "\n") {
|
||||
msg = msg.slice(0, -1)
|
||||
}
|
||||
if (msg.length > 0) {
|
||||
props.sendMessage(msg)
|
||||
}
|
||||
message.value = ""
|
||||
}
|
||||
|
||||
const usePrompt = (prompt) => {
|
||||
message.value = prompt
|
||||
}
|
||||
|
||||
const clickSendBtn = () => {
|
||||
send()
|
||||
}
|
||||
|
||||
const enterOnly = (event) => {
|
||||
event.preventDefault();
|
||||
if (!isMobile()) {
|
||||
send()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
usePrompt
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex-grow-1 d-flex align-center justify-space-between"
|
||||
@@ -25,67 +95,3 @@
|
||||
></v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isMobile } from 'is-mobile'
|
||||
export default {
|
||||
name: "MsgEditor",
|
||||
props: {
|
||||
sendMessage: Function,
|
||||
disabled: Boolean,
|
||||
loading: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
message: "",
|
||||
rows: 1,
|
||||
autoGrow: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hint() {
|
||||
return isMobile() ? "" : "Press Enter to send your message or Shift+Enter to add a new line";
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
message(val) {
|
||||
const lines = val.split(/\r\n|\r|\n/).length;
|
||||
if (lines > 8) {
|
||||
this.rows = 8;
|
||||
this.autoGrow = false;
|
||||
} else {
|
||||
this.rows = 1;
|
||||
this.autoGrow = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
send() {
|
||||
let msg = this.message
|
||||
// remove the last "\n"
|
||||
if (msg[msg.length - 1] === "\n") {
|
||||
msg = msg.slice(0, -1)
|
||||
}
|
||||
if (msg.length > 0) {
|
||||
this.sendMessage(msg)
|
||||
}
|
||||
this.message = ""
|
||||
},
|
||||
usePrompt(prompt) {
|
||||
this.message = prompt
|
||||
},
|
||||
clickSendBtn () {
|
||||
this.send()
|
||||
},
|
||||
enterOnly (event) {
|
||||
event.preventDefault();
|
||||
if (!isMobile()) {
|
||||
this.send()
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -17,6 +17,7 @@ services:
|
||||
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||
environment:
|
||||
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000}
|
||||
- SERVER_WORKERS=3 # The number of worker processes for handling requests.
|
||||
# - 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
|
||||
|
||||
@@ -101,6 +101,7 @@ services:
|
||||
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||
environment:
|
||||
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000} # CSRF 白名单,在这里设置为 chatgpt-ui-web-server 的地址+端口, 默认: localhost:9000
|
||||
- SERVER_WORKERS=3 # gunicorn 的工作进程数,默认为 3
|
||||
#- 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 # 默认超级用户
|
||||
|
||||
@@ -39,6 +39,14 @@
|
||||
"copied": "Copied",
|
||||
"delete": "Delete",
|
||||
"signOut": "Sign out",
|
||||
"resetPassword": "Reset password",
|
||||
"submit": "Submit",
|
||||
"agree": "Agree",
|
||||
"newPassword": "New password",
|
||||
"currentPassword": "Current password",
|
||||
"confirmPassword": "Confirm password",
|
||||
"yourPasswordHasBeenReset": "Your password has been reset",
|
||||
"nowYouNeedToSignInAgain": "Now you need to sign in again",
|
||||
"webSearch": "Web Search",
|
||||
"webSearchDefaultPrompt": "Web search results:\n\n[web_results]\nCurrent date: [current_date]\n\nInstructions: Using the provided web search results, write a comprehensive reply to the given query. Make sure to cite results using [[number](URL)] notation after the reference. If the provided search results refer to multiple subjects with the same name, write separate answers for each subject.\nQuery: [query]",
|
||||
"genTitlePrompt": "Generate a short title for the following content, no more than 10 words. \n\nContent: ",
|
||||
|
||||
@@ -39,6 +39,14 @@
|
||||
"copied": "Скопировано",
|
||||
"delete": "Удалить",
|
||||
"signOut": "Выход",
|
||||
"resetPassword": "Сбросить пароль",
|
||||
"submit": "Отправить",
|
||||
"agree": "Согласен",
|
||||
"newPassword": "Новый пароль",
|
||||
"currentPassword": "Текущий пароль",
|
||||
"confirmPassword": "Подтвердите пароль",
|
||||
"yourPasswordHasBeenReset": "Ваш пароль был сброшен",
|
||||
"nowYouNeedToSignInAgain": "Теперь вам нужно снова войти в систему",
|
||||
"webSearch": "Поиск в интернете",
|
||||
"webSearchDefaultPrompt": "Результаты веб-поиска:\n\n[web_results]\nТекущая дата: [current_date]\n\nИнструкции: Используя предоставленные результаты веб-поиска, напишите развернутый ответ на заданный запрос. Обязательно цитируйте результаты, используя обозначение [[number](URL)] после ссылки. Если предоставленные результаты поиска относятся к нескольким темам с одинаковым названием, напишите отдельные ответы для каждой темы.\nЗапрос: [query]",
|
||||
"genTitlePrompt": "Придумайте короткий заголовок для следующего содержания, не более 10 слов. \n\nСодержание: ",
|
||||
|
||||
@@ -39,6 +39,14 @@
|
||||
"copied": "已复制",
|
||||
"delete": "删除",
|
||||
"signOut": "退出登录",
|
||||
"resetPassword": "重置密码",
|
||||
"submit": "提交",
|
||||
"agree": "同意",
|
||||
"newPassword": "新密码",
|
||||
"currentPassword": "当前密码",
|
||||
"confirmPassword": "确认密码",
|
||||
"yourPasswordHasBeenReset": "您的密码已重置",
|
||||
"nowYouNeedToSignInAgain": "现在您需要再次登录",
|
||||
"webSearch": "网页搜索",
|
||||
"webSearchDefaultPrompt": "网络搜索结果:\n\n[web_results]\n当前日期:[current_date]\n\n说明:使用提供的网络搜索结果,对给定的查询写出全面的回复。确保在引用参考文献后使用 [[number](URL)] 符号进行引用结果. 如果提供的搜索结果涉及到多个具有相同名称的主题,请针对每个主题编写单独的答案。\n查询:[query]",
|
||||
"genTitlePrompt": "为以下内容生成一个不超过10个字的简短标题。 \n\n内容: ",
|
||||
|
||||
@@ -54,7 +54,6 @@ const deleteConversation = async (index) => {
|
||||
deletingConversationIndex.value = null
|
||||
if (!error.value) {
|
||||
if (conversations.value[index].id === currentConversation.value.id) {
|
||||
console.log('delete current conversation')
|
||||
createNewConversation()
|
||||
}
|
||||
conversations.value.splice(index, 1)
|
||||
@@ -124,19 +123,53 @@ onMounted(async () => {
|
||||
:permanent="drawerPermanent"
|
||||
width="300"
|
||||
>
|
||||
<div class="px-2 py-2">
|
||||
<template
|
||||
v-slot:prepend
|
||||
>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-btn
|
||||
block
|
||||
variant="outlined"
|
||||
prepend-icon="add"
|
||||
class="text-none"
|
||||
@click="createNewConversation"
|
||||
>
|
||||
{{ $t('newConversation') }}
|
||||
</v-btn>
|
||||
<v-list-item
|
||||
:title="$auth.user.username"
|
||||
:subtitle="$auth.user.email"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon
|
||||
icon="face"
|
||||
size="x-large"
|
||||
></v-icon>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
size="small"
|
||||
variant="text"
|
||||
icon="expand_more"
|
||||
></v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
:title="$t('resetPassword')"
|
||||
to="/account/resetPassword"
|
||||
>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
:title="$t('signOut')"
|
||||
@click="signOut"
|
||||
>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-divider></v-divider>
|
||||
</template>
|
||||
|
||||
<div class="px-2">
|
||||
<v-list>
|
||||
<v-list-item v-show="loadingConversations">
|
||||
<v-list-item-title class="d-flex justify-center">
|
||||
<v-progress-circular indeterminate></v-progress-circular>
|
||||
@@ -176,7 +209,7 @@ onMounted(async () => {
|
||||
:to="conversation.id ? `/${conversation.id}` : undefined"
|
||||
v-bind="props"
|
||||
>
|
||||
<v-list-item-title>{{ conversation.topic }}</v-list-item-title>
|
||||
<v-list-item-title>{{ conversation.topic !== "" ? conversation.topic : $t('defaultConversationTitle') }}</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<div
|
||||
v-show="isHovering && conversation.id"
|
||||
@@ -287,32 +320,33 @@ onMounted(async () => {
|
||||
@click="feedback"
|
||||
></v-list-item>
|
||||
|
||||
<v-list-item
|
||||
rounded="xl"
|
||||
prepend-icon="logout"
|
||||
:title="$t('signOut')"
|
||||
@click="signOut"
|
||||
></v-list-item>
|
||||
|
||||
</v-list>
|
||||
</div>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<v-app-bar
|
||||
class="d-md-none"
|
||||
class=""
|
||||
>
|
||||
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
|
||||
|
||||
<v-toolbar-title>{{ runtimeConfig.public.appName }}</v-toolbar-title>
|
||||
<v-toolbar-title>{{ currentConversation.id ? currentConversation.topic : runtimeConfig.public.appName }}</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn
|
||||
:title="$t('newConversation')"
|
||||
icon="add"
|
||||
@click="createNewConversation()"
|
||||
@click="createNewConversation"
|
||||
class="d-md-none"
|
||||
></v-btn>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
class="text-none d-none d-md-block"
|
||||
@click="createNewConversation"
|
||||
>
|
||||
{{ $t('newConversation') }}
|
||||
</v-btn>
|
||||
|
||||
</v-app-bar>
|
||||
|
||||
|
||||
176
pages/account/resetPassword.vue
Normal file
176
pages/account/resetPassword.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
middleware: ["auth"]
|
||||
})
|
||||
const formData = ref({
|
||||
old_password: '',
|
||||
new_password1: '',
|
||||
new_password2: ''
|
||||
})
|
||||
const formRules = ref({
|
||||
old_password: [
|
||||
v => !!v || 'Current password is required'
|
||||
],
|
||||
new_password1: [
|
||||
v => !!v || 'New password is required'
|
||||
],
|
||||
new_password2: [
|
||||
v => !!v || 'Confirm password is required',
|
||||
v => v === formData.value.new_password1 || 'Passwords do not match'
|
||||
]
|
||||
})
|
||||
const fieldErrors = ref({
|
||||
old_password: '',
|
||||
new_password1: '',
|
||||
new_password2: '',
|
||||
})
|
||||
const { $auth } = useNuxtApp()
|
||||
const errorMsg = ref(null)
|
||||
const resetForm = ref(null)
|
||||
const valid = ref(true)
|
||||
const submitting = ref(false)
|
||||
const route = useRoute()
|
||||
const passwordInputType = ref('password')
|
||||
|
||||
const signOut = async () => {
|
||||
const { data, error } = await useFetch('/api/account/logout/', {
|
||||
method: 'POST'
|
||||
})
|
||||
if (!error.value) {
|
||||
await $auth.logout()
|
||||
}
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
errorMsg.value = null
|
||||
const { valid } = await resetForm.value.validate()
|
||||
if (valid) {
|
||||
submitting.value = true
|
||||
const { data, error } = await useFetch('/api/account/password/change/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData.value)
|
||||
})
|
||||
submitting.value = false
|
||||
if (error.value) {
|
||||
if (error.value.status === 400) {
|
||||
for (const key in formData.value) {
|
||||
if (error.value.data[key]) {
|
||||
fieldErrors.value[key] = error.value.data[key][0]
|
||||
}
|
||||
}
|
||||
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 {
|
||||
successDialog.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleFieldUpdate = (field) => {
|
||||
fieldErrors.value[field] = ''
|
||||
}
|
||||
|
||||
const successDialog = ref(false)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
style="height: 100vh"
|
||||
>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col
|
||||
sm="9"
|
||||
offset-sm="1"
|
||||
md="6"
|
||||
offset-md="3"
|
||||
>
|
||||
<v-card
|
||||
class="mt-15"
|
||||
elevation="0"
|
||||
>
|
||||
<div class="text-center text-h4">{{ $t('resetPassword') }}</div>
|
||||
<v-card-text>
|
||||
<v-form ref="resetForm">
|
||||
<v-text-field
|
||||
v-model="formData.old_password"
|
||||
:rules="formRules.old_password"
|
||||
:error-messages="fieldErrors.old_password"
|
||||
@update:modelValue="handleFieldUpdate('old_password')"
|
||||
:label="$t('currentPassword')"
|
||||
variant="underlined"
|
||||
clearable
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="formData.new_password1"
|
||||
:rules="formRules.new_password1"
|
||||
:error-messages="fieldErrors.new_password1"
|
||||
@update:modelValue="handleFieldUpdate('new_password1')"
|
||||
:label="$t('newPassword')"
|
||||
variant="underlined"
|
||||
clearable
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="formData.new_password2"
|
||||
:rules="formRules.new_password2"
|
||||
:error-messages="fieldErrors.new_password2"
|
||||
@update:modelValue="handleFieldUpdate('new_password2')"
|
||||
:label="$t('confirmPassword')"
|
||||
variant="underlined"
|
||||
clearable
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
|
||||
<div v-if="errorMsg" class="text-red">{{ errorMsg }}</div>
|
||||
|
||||
<div
|
||||
class="mt-5 d-flex justify-space-between"
|
||||
>
|
||||
<v-btn
|
||||
block
|
||||
color="primary"
|
||||
:loading="submitting"
|
||||
@click="submit"
|
||||
size="large"
|
||||
>{{ $t('submit') }}</v-btn>
|
||||
</div>
|
||||
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card>
|
||||
|
||||
<v-dialog
|
||||
v-model="successDialog"
|
||||
persistent
|
||||
width="auto"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="text-h5">
|
||||
{{ $t('yourPasswordHasBeenReset') }}
|
||||
</v-card-title>
|
||||
<v-card-text>{{ $t('nowYouNeedToSignInAgain') }}</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="green-darken-1"
|
||||
variant="text"
|
||||
@click="signOut"
|
||||
>
|
||||
{{ $t('agree') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
@@ -83,7 +83,7 @@ const submit = async () => {
|
||||
}
|
||||
|
||||
const handleFieldUpdate = (field) => {
|
||||
// fieldErrors.value[field] = ''
|
||||
fieldErrors.value[field] = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -122,7 +122,7 @@ const handleFieldUpdate = (field) => {
|
||||
:error-messages="fieldErrors.email"
|
||||
label="Email"
|
||||
variant="underlined"
|
||||
@@update:modelValue="handleFieldUpdate('email')"
|
||||
@update:modelValue="handleFieldUpdate('email')"
|
||||
clearable
|
||||
></v-text-field>
|
||||
|
||||
|
||||
@@ -7,27 +7,33 @@ definePageMeta({
|
||||
const route = useRoute()
|
||||
const currentConversation = useConversation()
|
||||
const conversation = ref(getDefaultConversationData())
|
||||
watchEffect(() => {
|
||||
if (!route.params.id) {
|
||||
conversation.value = getDefaultConversationData()
|
||||
|
||||
const loadConversation = async () => {
|
||||
const { data, error } = await useAuthFetch('/api/chat/conversations/' + route.params.id)
|
||||
if (!error.value) {
|
||||
conversation.value = Object.assign(conversation.value, data.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const loadMessage = async () => {
|
||||
conversation.value.loadingMessages = true
|
||||
const { data, error } = await useAuthFetch('/api/chat/messages/?conversationId=' + route.params.id)
|
||||
if (!error.value) {
|
||||
conversation.value.messages = data.value
|
||||
}
|
||||
conversation.value.loadingMessages = false
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
onActivated(async () => {
|
||||
if (route.params.id) {
|
||||
conversation.value.id = parseInt(route.params.id)
|
||||
conversation.value.loadingMessages = true
|
||||
await loadConversation()
|
||||
await loadMessage()
|
||||
conversation.value.loadingMessages = false
|
||||
} else {
|
||||
conversation.value = getDefaultConversationData()
|
||||
}
|
||||
currentConversation.value = Object.assign({}, conversation.value)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -18,8 +18,6 @@ export const getConversations = async () => {
|
||||
}
|
||||
|
||||
export const createNewConversation = () => {
|
||||
const conversation = useConversation()
|
||||
conversation.value = getDefaultConversationData()
|
||||
navigateTo('/')
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user