Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a1adf6d00 | ||
|
|
ddce1c9721 | ||
|
|
f67ed7621c | ||
|
|
97649e4bee | ||
|
|
1082da050b |
@@ -105,6 +105,7 @@ services:
|
|||||||
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||||
environment:
|
environment:
|
||||||
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000} # CSRF whitelist,Add the address of your chatgpt-ui-web-server here, default is localhost:9000
|
- 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.
|
#- 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
|
#- OPENAI_API_PROXY=https://openai.proxy.com/v1 # Proxy for https://api.openai.com/v1
|
||||||
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name
|
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name
|
||||||
|
|||||||
289
components/Conversation.vue
Normal file
289
components/Conversation.vue
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
<script setup>
|
||||||
|
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
|
||||||
|
import {addConversation} from "../utils/helper";
|
||||||
|
|
||||||
|
const { $i18n, $auth } = useNuxtApp()
|
||||||
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
const currentModel = useCurrentModel()
|
||||||
|
const openaiApiKey = useApiKey()
|
||||||
|
const fetchingResponse = ref(false)
|
||||||
|
const messageQueue = []
|
||||||
|
let isProcessingQueue = false
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
conversation: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const processMessageQueue = () => {
|
||||||
|
if (isProcessingQueue || messageQueue.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!props.conversation.messages[props.conversation.messages.length - 1].is_bot) {
|
||||||
|
props.conversation.messages.push({id: null, is_bot: true, message: ''})
|
||||||
|
}
|
||||||
|
isProcessingQueue = true
|
||||||
|
const nextMessage = messageQueue.shift()
|
||||||
|
if (runtimeConfig.public.typewriter) {
|
||||||
|
let wordIndex = 0;
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
props.conversation.messages[props.conversation.messages.length - 1].message += nextMessage[wordIndex]
|
||||||
|
wordIndex++
|
||||||
|
if (wordIndex === nextMessage.length) {
|
||||||
|
clearInterval(intervalId)
|
||||||
|
isProcessingQueue = false
|
||||||
|
processMessageQueue()
|
||||||
|
}
|
||||||
|
}, runtimeConfig.public.typewriterDelay)
|
||||||
|
} else {
|
||||||
|
props.conversation.messages[props.conversation.messages.length - 1].message += nextMessage
|
||||||
|
isProcessingQueue = false
|
||||||
|
processMessageQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ctrl
|
||||||
|
const abortFetch = () => {
|
||||||
|
if (ctrl) {
|
||||||
|
ctrl.abort()
|
||||||
|
}
|
||||||
|
fetchingResponse.value = false
|
||||||
|
}
|
||||||
|
const fetchReply = async (message) => {
|
||||||
|
ctrl = new AbortController()
|
||||||
|
|
||||||
|
let webSearchParams = {}
|
||||||
|
if (enableWebSearch.value) {
|
||||||
|
webSearchParams['web_search'] = {
|
||||||
|
ua: navigator.userAgent,
|
||||||
|
default_prompt: $i18n.t('webSearchDefaultPrompt')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = Object.assign({}, currentModel.value, {
|
||||||
|
openaiApiKey: enableCustomApiKey.value ? openaiApiKey.value : null,
|
||||||
|
message: message,
|
||||||
|
conversationId: props.conversation.id
|
||||||
|
}, webSearchParams)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetchEventSource('/api/conversation/', {
|
||||||
|
signal: ctrl.signal,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
onopen(response) {
|
||||||
|
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to send message. HTTP ${response.status} - ${response.statusText}`);
|
||||||
|
},
|
||||||
|
onclose() {
|
||||||
|
if (ctrl.signal.aborted === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to send message. Server closed the connection unexpectedly.`);
|
||||||
|
},
|
||||||
|
onerror(err) {
|
||||||
|
throw err;
|
||||||
|
},
|
||||||
|
async onmessage(message) {
|
||||||
|
const event = message.event
|
||||||
|
const data = JSON.parse(message.data)
|
||||||
|
|
||||||
|
if (event === 'error') {
|
||||||
|
abortFetch()
|
||||||
|
showSnackbar(data.error)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === 'userMessageId') {
|
||||||
|
props.conversation.messages[props.conversation.messages.length - 1].id = data.userMessageId
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === 'done') {
|
||||||
|
if (props.conversation.id === null) {
|
||||||
|
props.conversation.id = data.conversationId
|
||||||
|
genTitle(props.conversation.id)
|
||||||
|
}
|
||||||
|
props.conversation.messages[props.conversation.messages.length - 1].id = data.messageId
|
||||||
|
abortFetch()
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
messageQueue.push(data.content)
|
||||||
|
processMessageQueue()
|
||||||
|
|
||||||
|
scrollChatWindow()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
abortFetch()
|
||||||
|
showSnackbar(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const grab = ref(null)
|
||||||
|
const scrollChatWindow = () => {
|
||||||
|
if (grab.value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
grab.value.scrollIntoView({behavior: 'smooth'})
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkOrAddConversation = () => {
|
||||||
|
if (props.conversation.messages.length === 0) {
|
||||||
|
props.conversation.messages.push({id: null, is_bot: true, message: ''})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const send = (message) => {
|
||||||
|
fetchingResponse.value = true
|
||||||
|
if (props.conversation.messages.length === 0) {
|
||||||
|
addConversation(props.conversation)
|
||||||
|
}
|
||||||
|
props.conversation.messages.push({message: message})
|
||||||
|
fetchReply(message)
|
||||||
|
scrollChatWindow()
|
||||||
|
}
|
||||||
|
const stop = () => {
|
||||||
|
abortFetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
const snackbar = ref(false)
|
||||||
|
const snackbarText = ref('')
|
||||||
|
const showSnackbar = (text) => {
|
||||||
|
snackbarText.value = text
|
||||||
|
snackbar.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = ref(null)
|
||||||
|
const usePrompt = (prompt) => {
|
||||||
|
editor.value.usePrompt(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteMessage = (index) => {
|
||||||
|
props.conversation.messages.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const showWebSearchToggle = ref(false)
|
||||||
|
const enableWebSearch = ref(false)
|
||||||
|
const enableCustomApiKey = ref(false)
|
||||||
|
|
||||||
|
const settings = useSettings()
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (settings.value) {
|
||||||
|
const settingsValue = toRaw(settings.value)
|
||||||
|
showWebSearchToggle.value = settingsValue.open_web_search && settingsValue.open_web_search === 'True'
|
||||||
|
enableCustomApiKey.value = settingsValue.open_api_key_setting && settingsValue.open_api_key_setting === 'True'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="conversation.loadingMessages"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
color="primary"
|
||||||
|
></v-progress-circular>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div
|
||||||
|
v-if="conversation.messages.length > 0"
|
||||||
|
ref="chatWindow"
|
||||||
|
>
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
v-for="(message, index) in conversation.messages" :key="index"
|
||||||
|
cols="12"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex align-center"
|
||||||
|
: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"
|
||||||
|
/>
|
||||||
|
<MsgContent :message="message" />
|
||||||
|
<MessageActions
|
||||||
|
v-if="message.is_bot"
|
||||||
|
:message="message"
|
||||||
|
:message-index="index"
|
||||||
|
:use-prompt="usePrompt"
|
||||||
|
:delete-message="deleteMessage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<div ref="grab" class="w-100" style="height: 200px;"></div>
|
||||||
|
</div>
|
||||||
|
<Welcome v-if="conversation.id === null && conversation.messages.length === 0" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<v-footer app>
|
||||||
|
<div class="px-md-16 w-100 d-flex flex-column">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-btn
|
||||||
|
v-show="fetchingResponse"
|
||||||
|
icon="close"
|
||||||
|
title="stop"
|
||||||
|
class="mr-3"
|
||||||
|
@click="stop"
|
||||||
|
></v-btn>
|
||||||
|
<MsgEditor ref="editor" :send-message="send" :disabled="fetchingResponse" :loading="fetchingResponse" />
|
||||||
|
</div>
|
||||||
|
<v-toolbar
|
||||||
|
density="comfortable"
|
||||||
|
color="transparent"
|
||||||
|
>
|
||||||
|
<Prompt v-show="!fetchingResponse" :use-prompt="usePrompt" />
|
||||||
|
<v-switch
|
||||||
|
v-if="showWebSearchToggle"
|
||||||
|
v-model="enableWebSearch"
|
||||||
|
hide-details
|
||||||
|
color="primary"
|
||||||
|
:label="$t('webSearch')"
|
||||||
|
></v-switch>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
</v-toolbar>
|
||||||
|
</div>
|
||||||
|
</v-footer>
|
||||||
|
<v-snackbar
|
||||||
|
v-model="snackbar"
|
||||||
|
multi-line
|
||||||
|
location="top"
|
||||||
|
>
|
||||||
|
{{ snackbarText }}
|
||||||
|
|
||||||
|
<template v-slot:actions>
|
||||||
|
<v-btn
|
||||||
|
color="red"
|
||||||
|
variant="text"
|
||||||
|
@click="snackbar = false"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-snackbar>
|
||||||
|
|
||||||
|
</template>
|
||||||
@@ -5,8 +5,8 @@ export const useCurrentModel = () => useState('currentModel', () => getCurrentMo
|
|||||||
|
|
||||||
export const useApiKey = () => useState('apiKey', () => getStoredApiKey())
|
export const useApiKey = () => useState('apiKey', () => getStoredApiKey())
|
||||||
|
|
||||||
export const useConversion = () => useState('conversion', () => getDefaultConversionData())
|
export const useConversation = () => useState('conversation', () => getDefaultConversationData())
|
||||||
|
|
||||||
export const useConversions = () => useState('conversions', () => [])
|
export const useConversations = () => useState('conversations', () => [])
|
||||||
|
|
||||||
export const useSettings = () => useState('settings', () => {})
|
export const useSettings = () => useState('settings', () => {})
|
||||||
@@ -17,6 +17,7 @@ services:
|
|||||||
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||||
environment:
|
environment:
|
||||||
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000}
|
- 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.
|
# - DB_URL=postgres://postgres:postgrespw@localhost:49153/chatgpt # If this parameter is not set, the built-in Sqlite will be used by default. It should be noted that if you do not connect to an external database, the data will be lost after the container is destroyed.
|
||||||
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name
|
- DJANGO_SUPERUSER_USERNAME=admin # default superuser name
|
||||||
- DJANGO_SUPERUSER_PASSWORD=password # default superuser password
|
- DJANGO_SUPERUSER_PASSWORD=password # default superuser password
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ services:
|
|||||||
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
image: wongsaang/chatgpt-ui-wsgi-server:latest
|
||||||
environment:
|
environment:
|
||||||
- APP_DOMAIN=${APP_DOMAIN:-localhost:9000} # CSRF 白名单,在这里设置为 chatgpt-ui-web-server 的地址+端口, 默认: localhost:9000
|
- 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 格式对照表
|
#- DB_URL=postgres://postgres:postgrespw@localhost:49153/chatgpt # 连接外部数据库,如果不设置这个参数,则默认使用内置的 Sqlite。需要注意的是,如果不连接外部数据库,数据将在容器销毁后丢失。链接格式请看下面的 DB_URL 格式对照表
|
||||||
#- OPENAI_API_PROXY=https://openai.proxy.com/v1 # https://api.openai.com/v1 的代理地址
|
#- OPENAI_API_PROXY=https://openai.proxy.com/v1 # https://api.openai.com/v1 的代理地址
|
||||||
- DJANGO_SUPERUSER_USERNAME=admin # 默认超级用户
|
- DJANGO_SUPERUSER_USERNAME=admin # 默认超级用户
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"themeMode": "Theme Mode",
|
"themeMode": "Theme Mode",
|
||||||
"feedback": "Feedback",
|
"feedback": "Feedback",
|
||||||
"newConversation": "New conversation",
|
"newConversation": "New conversation",
|
||||||
|
"defaultConversationTitle": "Unnamed",
|
||||||
"clearConversations": "Clear conversations",
|
"clearConversations": "Clear conversations",
|
||||||
"modelParameters": "Model Parameters",
|
"modelParameters": "Model Parameters",
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
@@ -38,8 +39,17 @@
|
|||||||
"copied": "Copied",
|
"copied": "Copied",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"signOut": "Sign out",
|
"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",
|
"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]",
|
"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: ",
|
||||||
"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.",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"themeMode": "Тема",
|
"themeMode": "Тема",
|
||||||
"feedback": "Обратная связь",
|
"feedback": "Обратная связь",
|
||||||
"newConversation": "Новый чат",
|
"newConversation": "Новый чат",
|
||||||
|
"defaultConversationTitle": "Безымянный",
|
||||||
"clearConversations": "Очистить чаты",
|
"clearConversations": "Очистить чаты",
|
||||||
"modelParameters": "Параметры модели",
|
"modelParameters": "Параметры модели",
|
||||||
"model": "Модель",
|
"model": "Модель",
|
||||||
@@ -38,8 +39,17 @@
|
|||||||
"copied": "Скопировано",
|
"copied": "Скопировано",
|
||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
"signOut": "Выход",
|
"signOut": "Выход",
|
||||||
|
"resetPassword": "Сбросить пароль",
|
||||||
|
"submit": "Отправить",
|
||||||
|
"agree": "Согласен",
|
||||||
|
"newPassword": "Новый пароль",
|
||||||
|
"currentPassword": "Текущий пароль",
|
||||||
|
"confirmPassword": "Подтвердите пароль",
|
||||||
|
"yourPasswordHasBeenReset": "Ваш пароль был сброшен",
|
||||||
|
"nowYouNeedToSignInAgain": "Теперь вам нужно снова войти в систему",
|
||||||
"webSearch": "Поиск в интернете",
|
"webSearch": "Поиск в интернете",
|
||||||
"webSearchDefaultPrompt": "Результаты веб-поиска:\n\n[web_results]\nТекущая дата: [current_date]\n\nИнструкции: Используя предоставленные результаты веб-поиска, напишите развернутый ответ на заданный запрос. Обязательно цитируйте результаты, используя обозначение [[number](URL)] после ссылки. Если предоставленные результаты поиска относятся к нескольким темам с одинаковым названием, напишите отдельные ответы для каждой темы.\nЗапрос: [query]",
|
"webSearchDefaultPrompt": "Результаты веб-поиска:\n\n[web_results]\nТекущая дата: [current_date]\n\nИнструкции: Используя предоставленные результаты веб-поиска, напишите развернутый ответ на заданный запрос. Обязательно цитируйте результаты, используя обозначение [[number](URL)] после ссылки. Если предоставленные результаты поиска относятся к нескольким темам с одинаковым названием, напишите отдельные ответы для каждой темы.\nЗапрос: [query]",
|
||||||
|
"genTitlePrompt": "Придумайте короткий заголовок для следующего содержания, не более 10 слов. \n\nСодержание: ",
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"introduction1": "является неофициальным клиентом для ChatGPT, но использует официальный API OpenAI.",
|
"introduction1": "является неофициальным клиентом для ChatGPT, но использует официальный API OpenAI.",
|
||||||
"introduction2": "Вам понадобится ключ API OpenAI, прежде чем вы сможете использовать этот клиент.",
|
"introduction2": "Вам понадобится ключ API OpenAI, прежде чем вы сможете использовать этот клиент.",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"themeMode": "主题模式",
|
"themeMode": "主题模式",
|
||||||
"feedback": "反馈",
|
"feedback": "反馈",
|
||||||
"newConversation": "新的对话",
|
"newConversation": "新的对话",
|
||||||
|
"defaultConversationTitle": "未命名",
|
||||||
"clearConversations": "清除对话",
|
"clearConversations": "清除对话",
|
||||||
"modelParameters": "模型参数",
|
"modelParameters": "模型参数",
|
||||||
"model": "模型",
|
"model": "模型",
|
||||||
@@ -38,8 +39,17 @@
|
|||||||
"copied": "已复制",
|
"copied": "已复制",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"signOut": "退出登录",
|
"signOut": "退出登录",
|
||||||
|
"resetPassword": "重置密码",
|
||||||
|
"submit": "提交",
|
||||||
|
"agree": "同意",
|
||||||
|
"newPassword": "新密码",
|
||||||
|
"currentPassword": "当前密码",
|
||||||
|
"confirmPassword": "确认密码",
|
||||||
|
"yourPasswordHasBeenReset": "您的密码已重置",
|
||||||
|
"nowYouNeedToSignInAgain": "现在您需要再次登录",
|
||||||
"webSearch": "网页搜索",
|
"webSearch": "网页搜索",
|
||||||
"webSearchDefaultPrompt": "网络搜索结果:\n\n[web_results]\n当前日期:[current_date]\n\n说明:使用提供的网络搜索结果,对给定的查询写出全面的回复。确保在引用参考文献后使用 [[number](URL)] 符号进行引用结果. 如果提供的搜索结果涉及到多个具有相同名称的主题,请针对每个主题编写单独的答案。\n查询:[query]",
|
"webSearchDefaultPrompt": "网络搜索结果:\n\n[web_results]\n当前日期:[current_date]\n\n说明:使用提供的网络搜索结果,对给定的查询写出全面的回复。确保在引用参考文献后使用 [[number](URL)] 符号进行引用结果. 如果提供的搜索结果涉及到多个具有相同名称的主题,请针对每个主题编写单独的答案。\n查询:[query]",
|
||||||
|
"genTitlePrompt": "为以下内容生成一个不超过10个字的简短标题。 \n\n内容: ",
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"introduction1": "是一个非官方的ChatGPT客户端,但使用OpenAI的官方API",
|
"introduction1": "是一个非官方的ChatGPT客户端,但使用OpenAI的官方API",
|
||||||
"introduction2": "在使用本客户端之前,您需要一个OpenAI API密钥。",
|
"introduction2": "在使用本客户端之前,您需要一个OpenAI API密钥。",
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ const setLang = (lang) => {
|
|||||||
setLocale(lang)
|
setLocale(lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversations = useConversions()
|
const conversations = useConversations()
|
||||||
const currentConversation = useConversion()
|
const currentConversation = useConversation()
|
||||||
|
|
||||||
const editingConversation = ref(null)
|
const editingConversation = ref(null)
|
||||||
const deletingConversationIndex = ref(null)
|
const deletingConversationIndex = ref(null)
|
||||||
@@ -54,7 +54,8 @@ const deleteConversation = async (index) => {
|
|||||||
deletingConversationIndex.value = null
|
deletingConversationIndex.value = null
|
||||||
if (!error.value) {
|
if (!error.value) {
|
||||||
if (conversations.value[index].id === currentConversation.value.id) {
|
if (conversations.value[index].id === currentConversation.value.id) {
|
||||||
createNewConversion()
|
console.log('delete current conversation')
|
||||||
|
createNewConversation()
|
||||||
}
|
}
|
||||||
conversations.value.splice(index, 1)
|
conversations.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
@@ -78,7 +79,7 @@ const loadingConversations = ref(false)
|
|||||||
|
|
||||||
const loadConversations = async () => {
|
const loadConversations = async () => {
|
||||||
loadingConversations.value = true
|
loadingConversations.value = true
|
||||||
conversations.value = await getConversions()
|
conversations.value = await getConversations()
|
||||||
loadingConversations.value = false
|
loadingConversations.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,19 +124,48 @@ onMounted(async () => {
|
|||||||
:permanent="drawerPermanent"
|
:permanent="drawerPermanent"
|
||||||
width="300"
|
width="300"
|
||||||
>
|
>
|
||||||
<div class="px-2 py-2">
|
<template
|
||||||
|
v-slot:prepend
|
||||||
|
>
|
||||||
<v-list>
|
<v-list>
|
||||||
<v-list-item>
|
<v-list-item
|
||||||
<v-btn
|
prepend-icon="face"
|
||||||
block
|
:title="$auth.user.username"
|
||||||
variant="outlined"
|
:subtitle="$auth.user.email"
|
||||||
prepend-icon="add"
|
>
|
||||||
@click="createNewConversion()"
|
<template v-slot:append>
|
||||||
class="text-none"
|
<v-menu>
|
||||||
>
|
<template v-slot:activator="{ props }">
|
||||||
{{ $t('newConversation') }}
|
<v-btn
|
||||||
</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-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="px-2">
|
||||||
|
<v-list>
|
||||||
<v-list-item v-show="loadingConversations">
|
<v-list-item v-show="loadingConversations">
|
||||||
<v-list-item-title class="d-flex justify-center">
|
<v-list-item-title class="d-flex justify-center">
|
||||||
<v-progress-circular indeterminate></v-progress-circular>
|
<v-progress-circular indeterminate></v-progress-circular>
|
||||||
@@ -172,19 +202,19 @@ onMounted(async () => {
|
|||||||
<v-list-item
|
<v-list-item
|
||||||
rounded="xl"
|
rounded="xl"
|
||||||
active-color="primary"
|
active-color="primary"
|
||||||
@click="openConversationMessages(conversation)"
|
:to="conversation.id ? `/${conversation.id}` : undefined"
|
||||||
v-bind="props"
|
v-bind="props"
|
||||||
>
|
>
|
||||||
<v-list-item-title>{{ conversation.topic }}</v-list-item-title>
|
<v-list-item-title>{{ conversation.topic }}</v-list-item-title>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<div
|
<div
|
||||||
v-show="isHovering"
|
v-show="isHovering && conversation.id"
|
||||||
>
|
>
|
||||||
<v-btn
|
<v-btn
|
||||||
icon="edit"
|
icon="edit"
|
||||||
size="small"
|
size="small"
|
||||||
variant="text"
|
variant="text"
|
||||||
@click.stop="editConversation(cIdx)"
|
@click.prevent="editConversation(cIdx)"
|
||||||
>
|
>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
@@ -192,7 +222,7 @@ onMounted(async () => {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="text"
|
variant="text"
|
||||||
:loading="deletingConversationIndex === cIdx"
|
:loading="deletingConversationIndex === cIdx"
|
||||||
@click.stop="deleteConversation(cIdx)"
|
@click.prevent="deleteConversation(cIdx)"
|
||||||
>
|
>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -286,32 +316,33 @@ onMounted(async () => {
|
|||||||
@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>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
|
|
||||||
<v-app-bar
|
<v-app-bar
|
||||||
class="d-md-none"
|
class=""
|
||||||
>
|
>
|
||||||
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
|
<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.topic ?? runtimeConfig.public.appName }}</v-toolbar-title>
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
:title="$t('newConversation')"
|
:title="$t('newConversation')"
|
||||||
icon="add"
|
icon="add"
|
||||||
@click="createNewConversion()"
|
@click="createNewConversation()"
|
||||||
|
class="d-md-none"
|
||||||
></v-btn>
|
></v-btn>
|
||||||
|
<v-btn
|
||||||
|
variant="outlined"
|
||||||
|
class="text-none d-none d-md-block"
|
||||||
|
@click="createNewConversation"
|
||||||
|
>
|
||||||
|
{{ $t('newConversation') }}
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"@kevinmarrec/nuxt-pwa": "^0.17.0",
|
"@kevinmarrec/nuxt-pwa": "^0.17.0",
|
||||||
"@nuxtjs/color-mode": "^3.2.0",
|
"@nuxtjs/color-mode": "^3.2.0",
|
||||||
"@nuxtjs/i18n": "^8.0.0-beta.9",
|
"@nuxtjs/i18n": "^8.0.0-beta.9",
|
||||||
"@vite-pwa/nuxt": "^0.0.7",
|
|
||||||
"material-design-icons-iconfont": "^6.7.0",
|
"material-design-icons-iconfont": "^6.7.0",
|
||||||
"nuxt": "^3.2.0"
|
"nuxt": "^3.2.0"
|
||||||
},
|
},
|
||||||
|
|||||||
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) => {
|
const handleFieldUpdate = (field) => {
|
||||||
// fieldErrors.value[field] = ''
|
fieldErrors.value[field] = ''
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ const handleFieldUpdate = (field) => {
|
|||||||
:error-messages="fieldErrors.email"
|
:error-messages="fieldErrors.email"
|
||||||
label="Email"
|
label="Email"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@@update:modelValue="handleFieldUpdate('email')"
|
@update:modelValue="handleFieldUpdate('email')"
|
||||||
clearable
|
clearable
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
|
|||||||
287
pages/index.vue
287
pages/index.vue
@@ -1,270 +1,43 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Prompt from "~/components/Prompt.vue";
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ["auth"]
|
middleware: ["auth"],
|
||||||
|
path: '/:id?',
|
||||||
|
keepalive: true
|
||||||
})
|
})
|
||||||
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
|
const route = useRoute()
|
||||||
|
const currentConversation = useConversation()
|
||||||
|
const conversation = ref(getDefaultConversationData())
|
||||||
|
|
||||||
const { $i18n, $auth } = useNuxtApp()
|
const loadConversation = async () => {
|
||||||
const runtimeConfig = useRuntimeConfig()
|
const { data, error } = await useAuthFetch('/api/chat/conversations/' + route.params.id)
|
||||||
const currentModel = useCurrentModel()
|
if (!error.value) {
|
||||||
const openaiApiKey = useApiKey()
|
conversation.value = data.value
|
||||||
const fetchingResponse = ref(false)
|
}
|
||||||
const messageQueue = []
|
}
|
||||||
let isProcessingQueue = false
|
|
||||||
|
|
||||||
const processMessageQueue = () => {
|
const loadMessage = async () => {
|
||||||
if (isProcessingQueue || messageQueue.length === 0) {
|
const { data, error } = await useAuthFetch('/api/chat/messages/?conversationId=' + route.params.id)
|
||||||
return
|
if (!error.value) {
|
||||||
|
conversation.value.messages = data.value
|
||||||
}
|
}
|
||||||
if (!currentConversation.value.messages[currentConversation.value.messages.length - 1].is_bot) {
|
}
|
||||||
currentConversation.value.messages.push({id: null, is_bot: true, message: ''})
|
|
||||||
}
|
onActivated(async () => {
|
||||||
isProcessingQueue = true
|
console.log('activated')
|
||||||
const nextMessage = messageQueue.shift()
|
console.log(conversation.value)
|
||||||
if (runtimeConfig.public.typewriter) {
|
if (route.params.id) {
|
||||||
let wordIndex = 0;
|
conversation.value.loadingMessages = true
|
||||||
const intervalId = setInterval(() => {
|
await loadConversation()
|
||||||
currentConversation.value.messages[currentConversation.value.messages.length - 1].message += nextMessage[wordIndex]
|
await loadMessage()
|
||||||
wordIndex++
|
conversation.value.loadingMessages = false
|
||||||
if (wordIndex === nextMessage.length) {
|
|
||||||
clearInterval(intervalId)
|
|
||||||
isProcessingQueue = false
|
|
||||||
processMessageQueue()
|
|
||||||
}
|
|
||||||
}, runtimeConfig.public.typewriterDelay)
|
|
||||||
} else {
|
} else {
|
||||||
currentConversation.value.messages[currentConversation.value.messages.length - 1].message += nextMessage
|
conversation.value = getDefaultConversationData()
|
||||||
isProcessingQueue = false
|
|
||||||
processMessageQueue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let ctrl
|
|
||||||
const abortFetch = () => {
|
|
||||||
if (ctrl) {
|
|
||||||
ctrl.abort()
|
|
||||||
}
|
|
||||||
fetchingResponse.value = false
|
|
||||||
}
|
|
||||||
const fetchReply = async (message) => {
|
|
||||||
ctrl = new AbortController()
|
|
||||||
|
|
||||||
let webSearchParams = {}
|
|
||||||
if (enableWebSearch.value) {
|
|
||||||
webSearchParams['web_search'] = {
|
|
||||||
ua: navigator.userAgent,
|
|
||||||
default_prompt: $i18n.t('webSearchDefaultPrompt')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = Object.assign({}, currentModel.value, {
|
|
||||||
openaiApiKey: enableCustomApiKey.value ? openaiApiKey.value : null,
|
|
||||||
message: message,
|
|
||||||
conversationId: currentConversation.value.id
|
|
||||||
}, webSearchParams)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fetchEventSource('/api/conversation/', {
|
|
||||||
signal: ctrl.signal,
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
onopen(response) {
|
|
||||||
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to send message. HTTP ${response.status} - ${response.statusText}`);
|
|
||||||
},
|
|
||||||
onclose() {
|
|
||||||
if (ctrl.signal.aborted === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to send message. Server closed the connection unexpectedly.`);
|
|
||||||
},
|
|
||||||
onerror(err) {
|
|
||||||
throw err;
|
|
||||||
},
|
|
||||||
async onmessage(message) {
|
|
||||||
const event = message.event
|
|
||||||
const data = JSON.parse(message.data)
|
|
||||||
|
|
||||||
if (event === 'error') {
|
|
||||||
abortFetch()
|
|
||||||
showSnackbar(data.error)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event === 'userMessageId') {
|
|
||||||
currentConversation.value.messages[currentConversation.value.messages.length - 1].id = data.userMessageId
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event === 'done') {
|
|
||||||
if (currentConversation.value.id === null) {
|
|
||||||
currentConversation.value.id = data.conversationId
|
|
||||||
genTitle(currentConversation.value.id)
|
|
||||||
}
|
|
||||||
currentConversation.value.messages[currentConversation.value.messages.length - 1].id = data.messageId
|
|
||||||
abortFetch()
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
messageQueue.push(data.content)
|
|
||||||
processMessageQueue()
|
|
||||||
|
|
||||||
scrollChatWindow()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
abortFetch()
|
|
||||||
showSnackbar(err.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentConversation = useConversion()
|
|
||||||
|
|
||||||
const grab = ref(null)
|
|
||||||
const scrollChatWindow = () => {
|
|
||||||
if (grab.value === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
grab.value.scrollIntoView({behavior: 'smooth'})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const send = (message) => {
|
|
||||||
fetchingResponse.value = true
|
|
||||||
currentConversation.value.messages.push({message: message})
|
|
||||||
fetchReply(message)
|
|
||||||
scrollChatWindow()
|
|
||||||
}
|
|
||||||
const stop = () => {
|
|
||||||
abortFetch()
|
|
||||||
}
|
|
||||||
|
|
||||||
const snackbar = ref(false)
|
|
||||||
const snackbarText = ref('')
|
|
||||||
const showSnackbar = (text) => {
|
|
||||||
snackbarText.value = text
|
|
||||||
snackbar.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const editor = ref(null)
|
|
||||||
const usePrompt = (prompt) => {
|
|
||||||
editor.value.usePrompt(prompt)
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteMessage = (index) => {
|
|
||||||
currentConversation.value.messages.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const showWebSearchToggle = ref(false)
|
|
||||||
const enableWebSearch = ref(false)
|
|
||||||
const enableCustomApiKey = ref(false)
|
|
||||||
|
|
||||||
const settings = useSettings()
|
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
if (settings.value) {
|
|
||||||
const settingsValue = toRaw(settings.value)
|
|
||||||
showWebSearchToggle.value = settingsValue.open_web_search && settingsValue.open_web_search === 'True'
|
|
||||||
enableCustomApiKey.value = settingsValue.open_api_key_setting && settingsValue.open_api_key_setting === 'True'
|
|
||||||
}
|
}
|
||||||
|
console.log(conversation.value)
|
||||||
|
currentConversation.value = Object.assign({}, conversation.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<Conversation :conversation="conversation" />
|
||||||
v-if="currentConversation.messages.length > 0"
|
|
||||||
ref="chatWindow"
|
|
||||||
>
|
|
||||||
<v-container>
|
|
||||||
<v-row>
|
|
||||||
<v-col
|
|
||||||
v-for="(message, index) in currentConversation.messages" :key="index"
|
|
||||||
cols="12"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="d-flex align-center"
|
|
||||||
: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"
|
|
||||||
/>
|
|
||||||
<MsgContent :message="message" />
|
|
||||||
<MessageActions
|
|
||||||
v-if="message.is_bot"
|
|
||||||
:message="message"
|
|
||||||
:message-index="index"
|
|
||||||
:use-prompt="usePrompt"
|
|
||||||
:delete-message="deleteMessage"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-container>
|
|
||||||
|
|
||||||
<div ref="grab" class="w-100" style="height: 200px;"></div>
|
|
||||||
</div>
|
|
||||||
<Welcome v-else />
|
|
||||||
<v-footer app>
|
|
||||||
<div class="px-md-16 w-100 d-flex flex-column">
|
|
||||||
<div class="d-flex align-center">
|
|
||||||
<v-btn
|
|
||||||
v-show="fetchingResponse"
|
|
||||||
icon="close"
|
|
||||||
title="stop"
|
|
||||||
class="mr-3"
|
|
||||||
@click="stop"
|
|
||||||
></v-btn>
|
|
||||||
<MsgEditor ref="editor" :send-message="send" :disabled="fetchingResponse" :loading="fetchingResponse" />
|
|
||||||
</div>
|
|
||||||
<v-toolbar
|
|
||||||
density="comfortable"
|
|
||||||
color="transparent"
|
|
||||||
>
|
|
||||||
<Prompt v-show="!fetchingResponse" :use-prompt="usePrompt" />
|
|
||||||
<v-switch
|
|
||||||
v-if="showWebSearchToggle"
|
|
||||||
v-model="enableWebSearch"
|
|
||||||
hide-details
|
|
||||||
color="primary"
|
|
||||||
:label="$t('webSearch')"
|
|
||||||
></v-switch>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</v-toolbar>
|
|
||||||
|
|
||||||
<!-- <div class="py-2 text-disabled text-caption font-weight-light text-center">-->
|
|
||||||
<!-- © {{ new Date().getFullYear() }} {{ runtimeConfig.public.appName }}-->
|
|
||||||
<!-- </div>-->
|
|
||||||
</div>
|
|
||||||
</v-footer>
|
|
||||||
<v-snackbar
|
|
||||||
v-model="snackbar"
|
|
||||||
multi-line
|
|
||||||
location="top"
|
|
||||||
>
|
|
||||||
{{ snackbarText }}
|
|
||||||
|
|
||||||
<template v-slot:actions>
|
|
||||||
<v-btn
|
|
||||||
color="red"
|
|
||||||
variant="text"
|
|
||||||
@click="snackbar = false"
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
</v-snackbar>
|
|
||||||
</template>
|
</template>
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
|
|
||||||
export const getDefaultConversionData = () => {
|
export const getDefaultConversationData = () => {
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
return {
|
return {
|
||||||
id: null,
|
id: null,
|
||||||
topic: null,
|
topic: $i18n.t('defaultConversationTitle'),
|
||||||
messages: [],
|
messages: [],
|
||||||
loadingMessages: false,
|
loadingMessages: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getConversions = async () => {
|
export const getConversations = async () => {
|
||||||
const { data, error } = await useAuthFetch('/api/chat/conversations/')
|
const { data, error } = await useAuthFetch('/api/chat/conversations/')
|
||||||
if (!error.value) {
|
if (!error.value) {
|
||||||
return data.value
|
return data.value
|
||||||
@@ -16,37 +17,35 @@ export const getConversions = async () => {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createNewConversion = () => {
|
export const createNewConversation = () => {
|
||||||
const conversation = useConversion()
|
const conversation = useConversation()
|
||||||
conversation.value = getDefaultConversionData()
|
conversation.value = getDefaultConversationData()
|
||||||
|
navigateTo('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const openConversationMessages = async (currentConversation) => {
|
|
||||||
const conversation = useConversion()
|
export const addConversation = (conversation) => {
|
||||||
conversation.value = Object.assign(conversation.value, currentConversation)
|
const conversations = useConversations()
|
||||||
conversation.value.loadingMessages = true
|
conversations.value = [conversation, ...conversations.value]
|
||||||
const { data, error } = await useAuthFetch('/api/chat/messages/?conversationId=' + currentConversation.id)
|
|
||||||
if (!error.value) {
|
|
||||||
conversation.value.messages = data.value
|
|
||||||
}
|
|
||||||
conversation.value.loadingMessages = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const genTitle = async (conversationId) => {
|
export const genTitle = async (conversationId) => {
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
const { data, error } = await useAuthFetch('/api/gen_title/', {
|
const { data, error } = await useAuthFetch('/api/gen_title/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
conversationId: conversationId
|
conversationId: conversationId,
|
||||||
|
prompt: $i18n.t('genTitlePrompt')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (!error.value) {
|
if (!error.value) {
|
||||||
const conversation = {
|
const conversations = useConversations()
|
||||||
id: conversationId,
|
let index = conversations.value.findIndex(item => item.id === conversationId)
|
||||||
topic: data.value.title,
|
if (index === -1) {
|
||||||
|
index = 0
|
||||||
}
|
}
|
||||||
const conversations = useConversions()
|
conversations.value[index].topic = data.value.title
|
||||||
// prepend to conversations
|
|
||||||
conversations.value = [conversation, ...conversations.value]
|
|
||||||
return data.value.title
|
return data.value.title
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|||||||
Reference in New Issue
Block a user