api proxy
This commit is contained in:
4
app.vue
4
app.vue
@@ -1,8 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
|
||||||
<NuxtLoadingIndicator />
|
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
|
<NuxtLoadingIndicator />
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
@@ -25,8 +25,6 @@ const processMessageQueue = () => {
|
|||||||
}
|
}
|
||||||
isProcessingQueue = true
|
isProcessingQueue = true
|
||||||
const nextMessage = messageQueue.shift()
|
const nextMessage = messageQueue.shift()
|
||||||
console.log(runtimeConfig.public.typewriter)
|
|
||||||
// console.log(process.env.NUXT_PUBLIC_TYPEWRITER)
|
|
||||||
if (runtimeConfig.public.typewriter) {
|
if (runtimeConfig.public.typewriter) {
|
||||||
let wordIndex = 0;
|
let wordIndex = 0;
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
@@ -111,7 +109,7 @@ const fetchReply = async (message) => {
|
|||||||
if (event === 'done') {
|
if (event === 'done') {
|
||||||
abortFetch()
|
abortFetch()
|
||||||
props.conversation.messages[props.conversation.messages.length - 1].id = data.messageId
|
props.conversation.messages[props.conversation.messages.length - 1].id = data.messageId
|
||||||
if (!props.conversation.topic || props.conversation.topic === '') {
|
if (!props.conversation.id) {
|
||||||
props.conversation.id = data.conversationId
|
props.conversation.id = data.conversationId
|
||||||
genTitle(props.conversation.id)
|
genTitle(props.conversation.id)
|
||||||
}
|
}
|
||||||
@@ -139,12 +137,6 @@ const scrollChatWindow = () => {
|
|||||||
grab.value.scrollIntoView({behavior: 'smooth'})
|
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) => {
|
const send = (message) => {
|
||||||
fetchingResponse.value = true
|
fetchingResponse.value = true
|
||||||
if (props.conversation.messages.length === 0) {
|
if (props.conversation.messages.length === 0) {
|
||||||
|
|||||||
338
components/NavigationDrawer.vue
Normal file
338
components/NavigationDrawer.vue
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
import {useDrawer} from "../composables/states";
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
const {mdAndUp} = useDisplay()
|
||||||
|
const drawerPermanent = computed(() => {
|
||||||
|
return mdAndUp.value
|
||||||
|
})
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
const themes = ref([
|
||||||
|
{ title: $i18n.t('lightMode'), value: 'light' },
|
||||||
|
{ title: $i18n.t('darkMode'), value: 'dark' },
|
||||||
|
{ title: $i18n.t('followSystem'), value: 'system'}
|
||||||
|
])
|
||||||
|
const setTheme = (theme) => {
|
||||||
|
colorMode.preference = theme
|
||||||
|
}
|
||||||
|
const feedback = () => {
|
||||||
|
window.open('https://github.com/WongSaang/chatgpt-ui/issues', '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { locale, locales, setLocale } = useI18n()
|
||||||
|
const setLang = (lang) => {
|
||||||
|
setLocale(lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversations = useConversations()
|
||||||
|
|
||||||
|
const editingConversation = ref(null)
|
||||||
|
const deletingConversationIndex = ref(null)
|
||||||
|
|
||||||
|
const editConversation = (index) => {
|
||||||
|
editingConversation.value = conversations.value[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateConversation = async (index) => {
|
||||||
|
editingConversation.value.updating = true
|
||||||
|
const { data, error } = await useAuthFetch(`/api/chat/conversations/${editingConversation.value.id}/`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({
|
||||||
|
topic: editingConversation.value.topic
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!error.value) {
|
||||||
|
conversations.value[index] = editingConversation.value
|
||||||
|
}
|
||||||
|
editingConversation.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteConversation = async (index) => {
|
||||||
|
deletingConversationIndex.value = index
|
||||||
|
const { data, error } = await useAuthFetch(`/api/chat/conversations/${conversations.value[index].id}/`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
deletingConversationIndex.value = null
|
||||||
|
if (!error.value) {
|
||||||
|
const deletingConversation = conversations.value[index]
|
||||||
|
conversations.value.splice(index, 1)
|
||||||
|
if (route.params.id && parseInt(route.params.id) === deletingConversation.id) {
|
||||||
|
await navigateTo('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearConversations = async () => {
|
||||||
|
deletingConversations.value = true
|
||||||
|
const { data, error } = await useAuthFetch(`/api/chat/conversations/delete_all`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
if (!error.value) {
|
||||||
|
loadConversations()
|
||||||
|
clearConfirmDialog.value = false
|
||||||
|
}
|
||||||
|
deletingConversations.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearConfirmDialog = ref(false)
|
||||||
|
const deletingConversations = ref(false)
|
||||||
|
const loadingConversations = ref(false)
|
||||||
|
|
||||||
|
const loadConversations = async () => {
|
||||||
|
loadingConversations.value = true
|
||||||
|
conversations.value = await getConversations()
|
||||||
|
loadingConversations.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = useSettings()
|
||||||
|
const showApiKeySetting = ref(false)
|
||||||
|
watchEffect(() => {
|
||||||
|
if (settings.value) {
|
||||||
|
const settingsValue = toRaw(settings.value)
|
||||||
|
showApiKeySetting.value = settingsValue.open_api_key_setting && settingsValue.open_api_key_setting === 'True'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const signOut = async () => {
|
||||||
|
const { data, error } = await useFetch('/api/account/logout/', {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
if (!error.value) {
|
||||||
|
await logout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNuxtReady(async () => {
|
||||||
|
loadConversations()
|
||||||
|
})
|
||||||
|
|
||||||
|
const drawer = useDrawer()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-navigation-drawer
|
||||||
|
v-model="drawer"
|
||||||
|
:permanent="drawerPermanent"
|
||||||
|
width="300"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-slot:prepend
|
||||||
|
v-if="user"
|
||||||
|
>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
:title="user.username"
|
||||||
|
:subtitle="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>
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<v-list>
|
||||||
|
<template
|
||||||
|
v-for="(conversation, cIdx) in conversations"
|
||||||
|
:key="conversation.id"
|
||||||
|
>
|
||||||
|
<v-list-item
|
||||||
|
active-color="primary"
|
||||||
|
rounded="xl"
|
||||||
|
v-if="editingConversation && editingConversation.id === conversation.id"
|
||||||
|
>
|
||||||
|
<v-text-field
|
||||||
|
v-model="editingConversation.topic"
|
||||||
|
:loading="editingConversation.updating"
|
||||||
|
variant="underlined"
|
||||||
|
append-icon="done"
|
||||||
|
hide-details
|
||||||
|
density="compact"
|
||||||
|
autofocus
|
||||||
|
@keyup.enter="updateConversation(cIdx)"
|
||||||
|
@click:append="updateConversation(cIdx)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-list-item>
|
||||||
|
<v-hover
|
||||||
|
v-if="!editingConversation || editingConversation.id !== conversation.id"
|
||||||
|
v-slot="{ isHovering, props }"
|
||||||
|
>
|
||||||
|
<v-list-item
|
||||||
|
rounded="xl"
|
||||||
|
active-color="primary"
|
||||||
|
:to="conversation.id ? `/${conversation.id}` : '/'"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<v-list-item-title>{{ (conversation.topic && conversation.topic !== '') ? conversation.topic : $t('defaultConversationTitle') }}</v-list-item-title>
|
||||||
|
<template v-slot:append>
|
||||||
|
<div
|
||||||
|
v-show="isHovering && conversation.id"
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
icon="edit"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click.prevent="editConversation(cIdx)"
|
||||||
|
>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon="delete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
:loading="deletingConversationIndex === cIdx"
|
||||||
|
@click.prevent="deleteConversation(cIdx)"
|
||||||
|
>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-hover>
|
||||||
|
</template>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-slot:append>
|
||||||
|
<div class="px-1">
|
||||||
|
<v-divider></v-divider>
|
||||||
|
<v-list>
|
||||||
|
|
||||||
|
<v-dialog
|
||||||
|
v-model="clearConfirmDialog"
|
||||||
|
persistent
|
||||||
|
>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-list-item
|
||||||
|
v-bind="props"
|
||||||
|
rounded="xl"
|
||||||
|
prepend-icon="delete_forever"
|
||||||
|
:title="$t('clearConversations')"
|
||||||
|
></v-list-item>
|
||||||
|
</template>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5">
|
||||||
|
Are you sure you want to delete all conversations?
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>This will be a permanent deletion and cannot be retrieved once deleted. Please proceed with caution.</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
color="green-darken-1"
|
||||||
|
variant="text"
|
||||||
|
@click="clearConfirmDialog = false"
|
||||||
|
class="text-none"
|
||||||
|
>
|
||||||
|
Cancel deletion
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="green-darken-1"
|
||||||
|
variant="text"
|
||||||
|
@click="clearConversations"
|
||||||
|
class="text-none"
|
||||||
|
:loading="deletingConversations"
|
||||||
|
>
|
||||||
|
Confirm deletion
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<ApiKeyDialog
|
||||||
|
v-if="showApiKeySetting"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModelParameters/>
|
||||||
|
|
||||||
|
<v-menu
|
||||||
|
>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-list-item
|
||||||
|
v-bind="props"
|
||||||
|
rounded="xl"
|
||||||
|
:prepend-icon="$colorMode.value === 'light' ? 'light_mode' : 'dark_mode'"
|
||||||
|
:title="$t('themeMode')"
|
||||||
|
></v-list-item>
|
||||||
|
</template>
|
||||||
|
<v-list
|
||||||
|
bg-color="white"
|
||||||
|
>
|
||||||
|
<v-list-item
|
||||||
|
v-for="(theme, idx) in themes"
|
||||||
|
:key="idx"
|
||||||
|
@click="setTheme(theme.value)"
|
||||||
|
>
|
||||||
|
<v-list-item-title>{{ theme.title }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
|
||||||
|
<SettingsLanguages/>
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
rounded="xl"
|
||||||
|
prepend-icon="help_outline"
|
||||||
|
:title="$t('feedback')"
|
||||||
|
@click="feedback"
|
||||||
|
></v-list-item>
|
||||||
|
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.v-navigation-drawer__content::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
.v-navigation-drawer__content:hover::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
.v-navigation-drawer__content:hover::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #999;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,6 +2,7 @@ export const useMyFetch = (url, options = {}) => {
|
|||||||
let defaultOptions = {
|
let defaultOptions = {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (process.server) {
|
if (process.server) {
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ export const useCurrentModel = () => useState('currentModel', () => getCurrentMo
|
|||||||
|
|
||||||
export const useApiKey = () => useState('apiKey', () => getStoredApiKey())
|
export const useApiKey = () => useState('apiKey', () => getStoredApiKey())
|
||||||
|
|
||||||
export const useConversation = () => useState('conversation', () => getDefaultConversationData())
|
|
||||||
|
|
||||||
export const useConversations = () => useState('conversations', () => [])
|
export const useConversations = () => useState('conversations', () => [])
|
||||||
|
|
||||||
export const useSettings = () => useState('settings', () => getSystemSettings())
|
export const useSettings = () => useState('settings', () => getSystemSettings())
|
||||||
|
|
||||||
export const useUser = () => useState('user', () => null)
|
export const useUser = () => useState('user', () => null)
|
||||||
|
|
||||||
|
export const useDrawer = () => useState('drawer', () => false)
|
||||||
@@ -5,6 +5,7 @@ services:
|
|||||||
build: .
|
build: .
|
||||||
environment:
|
environment:
|
||||||
SERVER_DOMAIN: ${SERVER_DOMAIN:-http://web-server}
|
SERVER_DOMAIN: ${SERVER_DOMAIN:-http://web-server}
|
||||||
|
NUXT_PUBLIC_TYPEWRITER: false
|
||||||
ports:
|
ports:
|
||||||
- '${CLIENT_PORT:-80}:80'
|
- '${CLIENT_PORT:-80}:80'
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -1,382 +1,8 @@
|
|||||||
<script setup>
|
|
||||||
import {useDisplay} from "vuetify";
|
|
||||||
|
|
||||||
const { $i18n } = useNuxtApp()
|
|
||||||
const runtimeConfig = useRuntimeConfig()
|
|
||||||
const colorMode = useColorMode()
|
|
||||||
const drawer = ref(null)
|
|
||||||
const themes = ref([
|
|
||||||
{ title: $i18n.t('lightMode'), value: 'light' },
|
|
||||||
{ title: $i18n.t('darkMode'), value: 'dark' },
|
|
||||||
{ title: $i18n.t('followSystem'), value: 'system'}
|
|
||||||
])
|
|
||||||
const setTheme = (theme) => {
|
|
||||||
colorMode.preference = theme
|
|
||||||
}
|
|
||||||
const feedback = () => {
|
|
||||||
window.open('https://github.com/WongSaang/chatgpt-ui/issues', '_blank')
|
|
||||||
}
|
|
||||||
|
|
||||||
const { locale, locales, setLocale } = useI18n()
|
|
||||||
const setLang = (lang) => {
|
|
||||||
setLocale(lang)
|
|
||||||
}
|
|
||||||
|
|
||||||
const conversations = useConversations()
|
|
||||||
const currentConversation = useConversation()
|
|
||||||
|
|
||||||
const editingConversation = ref(null)
|
|
||||||
const deletingConversationIndex = ref(null)
|
|
||||||
|
|
||||||
const editConversation = (index) => {
|
|
||||||
editingConversation.value = conversations.value[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateConversation = async (index) => {
|
|
||||||
editingConversation.value.updating = true
|
|
||||||
const { data, error } = await useAuthFetch(`/api/chat/conversations/${editingConversation.value.id}/`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify({
|
|
||||||
topic: editingConversation.value.topic
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if (!error.value) {
|
|
||||||
conversations.value[index] = editingConversation.value
|
|
||||||
}
|
|
||||||
editingConversation.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteConversation = async (index) => {
|
|
||||||
deletingConversationIndex.value = index
|
|
||||||
const { data, error } = await useAuthFetch(`/api/chat/conversations/${conversations.value[index].id}/`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
})
|
|
||||||
deletingConversationIndex.value = null
|
|
||||||
if (!error.value) {
|
|
||||||
const deletingConversation = conversations.value[index]
|
|
||||||
conversations.value.splice(index, 1)
|
|
||||||
if (deletingConversation.id === currentConversation.value.id) {
|
|
||||||
await navigateTo('/')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearConversations = async () => {
|
|
||||||
deletingConversations.value = true
|
|
||||||
const { data, error } = await useAuthFetch(`/api/chat/conversations/delete_all`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
})
|
|
||||||
if (!error.value) {
|
|
||||||
loadConversations()
|
|
||||||
clearConfirmDialog.value = false
|
|
||||||
}
|
|
||||||
deletingConversations.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearConfirmDialog = ref(false)
|
|
||||||
const deletingConversations = ref(false)
|
|
||||||
const loadingConversations = ref(false)
|
|
||||||
|
|
||||||
const loadConversations = async () => {
|
|
||||||
loadingConversations.value = true
|
|
||||||
conversations.value = await getConversations()
|
|
||||||
loadingConversations.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const {mdAndUp} = useDisplay()
|
|
||||||
|
|
||||||
|
|
||||||
const drawerPermanent = computed(() => {
|
|
||||||
return mdAndUp.value
|
|
||||||
})
|
|
||||||
|
|
||||||
const signOut = async () => {
|
|
||||||
const { data, error } = await useFetch('/api/account/logout/', {
|
|
||||||
method: 'POST'
|
|
||||||
})
|
|
||||||
if (!error.value) {
|
|
||||||
await logout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings = useSettings()
|
|
||||||
const showApiKeySetting = ref(false)
|
|
||||||
watchEffect(() => {
|
|
||||||
if (settings.value) {
|
|
||||||
const settingsValue = toRaw(settings.value)
|
|
||||||
showApiKeySetting.value = settingsValue.open_api_key_setting && settingsValue.open_api_key_setting === 'True'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const user = useUser()
|
|
||||||
|
|
||||||
const navTitle = computed(() => {
|
|
||||||
if (currentConversation.value && currentConversation.value.topic !== null) {
|
|
||||||
return currentConversation.value.topic === '' ? $i18n.t('defaultConversationTitle') : currentConversation.value.topic
|
|
||||||
}
|
|
||||||
return runtimeConfig.public.appName
|
|
||||||
})
|
|
||||||
|
|
||||||
onNuxtReady(async () => {
|
|
||||||
loadConversations()
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-app
|
<v-app
|
||||||
:theme="$colorMode.value"
|
:theme="$colorMode.value"
|
||||||
>
|
>
|
||||||
<v-navigation-drawer
|
<NavigationDrawer />
|
||||||
v-model="drawer"
|
<slot />
|
||||||
:permanent="drawerPermanent"
|
|
||||||
width="300"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-slot:prepend
|
|
||||||
v-if="user"
|
|
||||||
>
|
|
||||||
<v-list>
|
|
||||||
<v-list-item
|
|
||||||
:title="user.username"
|
|
||||||
:subtitle="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>
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
|
|
||||||
<v-list>
|
|
||||||
<template
|
|
||||||
v-for="(conversation, cIdx) in conversations"
|
|
||||||
:key="conversation.id"
|
|
||||||
>
|
|
||||||
<v-list-item
|
|
||||||
active-color="primary"
|
|
||||||
rounded="xl"
|
|
||||||
v-if="editingConversation && editingConversation.id === conversation.id"
|
|
||||||
>
|
|
||||||
<v-text-field
|
|
||||||
v-model="editingConversation.topic"
|
|
||||||
:loading="editingConversation.updating"
|
|
||||||
variant="underlined"
|
|
||||||
append-icon="done"
|
|
||||||
hide-details
|
|
||||||
density="compact"
|
|
||||||
autofocus
|
|
||||||
@keyup.enter="updateConversation(cIdx)"
|
|
||||||
@click:append="updateConversation(cIdx)"
|
|
||||||
></v-text-field>
|
|
||||||
</v-list-item>
|
|
||||||
<v-hover
|
|
||||||
v-if="!editingConversation || editingConversation.id !== conversation.id"
|
|
||||||
v-slot="{ isHovering, props }"
|
|
||||||
>
|
|
||||||
<v-list-item
|
|
||||||
rounded="xl"
|
|
||||||
active-color="primary"
|
|
||||||
:to="conversation.id ? `/${conversation.id}` : undefined"
|
|
||||||
v-bind="props"
|
|
||||||
>
|
|
||||||
<v-list-item-title>{{ (conversation.topic && conversation.topic !== '') ? conversation.topic : $t('defaultConversationTitle') }}</v-list-item-title>
|
|
||||||
<template v-slot:append>
|
|
||||||
<div
|
|
||||||
v-show="isHovering && conversation.id"
|
|
||||||
>
|
|
||||||
<v-btn
|
|
||||||
icon="edit"
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
@click.prevent="editConversation(cIdx)"
|
|
||||||
>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon="delete"
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
:loading="deletingConversationIndex === cIdx"
|
|
||||||
@click.prevent="deleteConversation(cIdx)"
|
|
||||||
>
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</v-list-item>
|
|
||||||
</v-hover>
|
|
||||||
</template>
|
|
||||||
</v-list>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-slot:append>
|
|
||||||
<div class="px-1">
|
|
||||||
<v-divider></v-divider>
|
|
||||||
<v-list>
|
|
||||||
|
|
||||||
<v-dialog
|
|
||||||
v-model="clearConfirmDialog"
|
|
||||||
persistent
|
|
||||||
>
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<v-list-item
|
|
||||||
v-bind="props"
|
|
||||||
rounded="xl"
|
|
||||||
prepend-icon="delete_forever"
|
|
||||||
:title="$t('clearConversations')"
|
|
||||||
></v-list-item>
|
|
||||||
</template>
|
|
||||||
<v-card>
|
|
||||||
<v-card-title class="text-h5">
|
|
||||||
Are you sure you want to delete all conversations?
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text>This will be a permanent deletion and cannot be retrieved once deleted. Please proceed with caution.</v-card-text>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-btn
|
|
||||||
color="green-darken-1"
|
|
||||||
variant="text"
|
|
||||||
@click="clearConfirmDialog = false"
|
|
||||||
class="text-none"
|
|
||||||
>
|
|
||||||
Cancel deletion
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
color="green-darken-1"
|
|
||||||
variant="text"
|
|
||||||
@click="clearConversations"
|
|
||||||
class="text-none"
|
|
||||||
:loading="deletingConversations"
|
|
||||||
>
|
|
||||||
Confirm deletion
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
|
|
||||||
<ApiKeyDialog
|
|
||||||
v-if="showApiKeySetting"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ModelParameters/>
|
|
||||||
|
|
||||||
<v-menu
|
|
||||||
>
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<v-list-item
|
|
||||||
v-bind="props"
|
|
||||||
rounded="xl"
|
|
||||||
:prepend-icon="$colorMode.value === 'light' ? 'light_mode' : 'dark_mode'"
|
|
||||||
:title="$t('themeMode')"
|
|
||||||
></v-list-item>
|
|
||||||
</template>
|
|
||||||
<v-list
|
|
||||||
bg-color="white"
|
|
||||||
>
|
|
||||||
<v-list-item
|
|
||||||
v-for="(theme, idx) in themes"
|
|
||||||
:key="idx"
|
|
||||||
@click="setTheme(theme.value)"
|
|
||||||
>
|
|
||||||
<v-list-item-title>{{ theme.title }}</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
|
|
||||||
<SettingsLanguages/>
|
|
||||||
|
|
||||||
<v-list-item
|
|
||||||
rounded="xl"
|
|
||||||
prepend-icon="help_outline"
|
|
||||||
:title="$t('feedback')"
|
|
||||||
@click="feedback"
|
|
||||||
></v-list-item>
|
|
||||||
|
|
||||||
</v-list>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</v-navigation-drawer>
|
|
||||||
|
|
||||||
<v-app-bar
|
|
||||||
class=""
|
|
||||||
>
|
|
||||||
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
|
|
||||||
|
|
||||||
<v-toolbar-title>{{ navTitle }}</v-toolbar-title>
|
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
|
|
||||||
<v-btn
|
|
||||||
:title="$t('newConversation')"
|
|
||||||
icon="add"
|
|
||||||
@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>
|
|
||||||
|
|
||||||
<v-main>
|
|
||||||
<NuxtPage/>
|
|
||||||
</v-main>
|
|
||||||
|
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
|
||||||
.v-navigation-drawer__content::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
.v-navigation-drawer__content:hover::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
.v-navigation-drawer__content:hover::-webkit-scrollbar-thumb {
|
|
||||||
background-color: #999;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -68,14 +68,5 @@ export default defineNuxtConfig({
|
|||||||
vueI18n: {
|
vueI18n: {
|
||||||
fallbackLocale: 'en',
|
fallbackLocale: 'en',
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
// nitro: {
|
|
||||||
// devProxy: {
|
|
||||||
// "/api": {
|
|
||||||
// target: process.env.NUXT_DEV_SERVER ?? 'http://localhost:8000/api',
|
|
||||||
// changeOrigin: true,
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ definePageMeta({
|
|||||||
path: '/:id?',
|
path: '/:id?',
|
||||||
keepalive: true
|
keepalive: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
const drawer = useDrawer()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const currentConversation = useConversation()
|
|
||||||
const conversation = ref(getDefaultConversationData())
|
const conversation = ref(getDefaultConversationData())
|
||||||
|
|
||||||
const loadConversation = async () => {
|
const loadConversation = async () => {
|
||||||
@@ -22,31 +25,67 @@ const loadMessage = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateCurrentConversation = () => {
|
const createNewConversation = () => {
|
||||||
currentConversation.value = Object.assign({}, conversation.value)
|
if (route.path !== '/') {
|
||||||
|
return navigateTo('/?new')
|
||||||
|
}
|
||||||
|
conversation.value = Object.assign(getDefaultConversationData(), {
|
||||||
|
topic: $i18n.t('newConversation')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (route.params.id) {
|
if (route.params.id) {
|
||||||
conversation.value.loadingMessages = true
|
conversation.value.loadingMessages = true
|
||||||
await loadConversation()
|
await loadConversation()
|
||||||
await loadMessage()
|
await loadMessage()
|
||||||
conversation.value.loadingMessages = false
|
conversation.value.loadingMessages = false
|
||||||
updateCurrentConversation()
|
|
||||||
} else {
|
|
||||||
watch(currentConversation, (val) => {
|
|
||||||
conversation.value = Object.assign({}, val)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const navTitle = computed(() => {
|
||||||
|
if (conversation.value && conversation.value.topic !== null) {
|
||||||
|
return conversation.value.topic === '' ? $i18n.t('defaultConversationTitle') : conversation.value.topic
|
||||||
|
}
|
||||||
|
return runtimeConfig.public.appName
|
||||||
|
})
|
||||||
|
|
||||||
onActivated(async () => {
|
onActivated(async () => {
|
||||||
updateCurrentConversation()
|
if (route.path === '/' && route.query.new !== undefined) {
|
||||||
|
createNewConversation()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<v-app-bar>
|
||||||
|
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
|
||||||
|
|
||||||
|
<v-toolbar-title>{{ navTitle }}</v-toolbar-title>
|
||||||
|
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
:title="$t('newConversation')"
|
||||||
|
icon="add"
|
||||||
|
@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>
|
||||||
|
|
||||||
|
<v-main>
|
||||||
<Welcome v-if="!route.params.id && conversation.messages.length === 0" />
|
<Welcome v-if="!route.params.id && conversation.messages.length === 0" />
|
||||||
<Conversation :conversation="conversation" />
|
<Conversation :conversation="conversation" />
|
||||||
|
</v-main>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,10 +1,34 @@
|
|||||||
import { createProxyMiddleware, Filter, Options, RequestHandler } from 'http-proxy-middleware'
|
import {getRequestURL} from "h3";
|
||||||
export default defineEventHandler((event) => {
|
|
||||||
|
const PayloadMethods = new Set(["PATCH", "POST", "PUT", "DELETE"]);
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (event.node.req.url.startsWith('/api/')) {
|
if (event.node.req.url.startsWith('/api/')) {
|
||||||
return proxyRequest(
|
// TODO: fix fetch failed
|
||||||
event,
|
const target = (process.env.SERVER_DOMAIN || 'http://localhost:8000') + event.node.req.url
|
||||||
(process.env.SERVER_DOMAIN || 'http://localhost:8000') + event.node.req.url
|
// Method
|
||||||
)
|
const method = getMethod(event)
|
||||||
|
// Body
|
||||||
|
let body;
|
||||||
|
if (PayloadMethods.has(method)) {
|
||||||
|
body = await readRawBody(event).catch(() => undefined);
|
||||||
|
}
|
||||||
|
// Headers
|
||||||
|
const headers = getProxyRequestHeaders(event);
|
||||||
|
|
||||||
|
if (method === 'DELETE') {
|
||||||
|
delete headers['content-length']
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendProxy(event, target, {
|
||||||
|
sendStream: event.node.req.url === '/api/conversation/',
|
||||||
|
fetchOptions: {
|
||||||
|
headers,
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,19 +17,6 @@ export const getConversations = async () => {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createNewConversation = () => {
|
|
||||||
const route = useRoute()
|
|
||||||
const { $i18n } = useNuxtApp()
|
|
||||||
const currentConversation = useConversation()
|
|
||||||
currentConversation.value = Object.assign(getDefaultConversationData(), {
|
|
||||||
topic: $i18n.t('newConversation')
|
|
||||||
})
|
|
||||||
if (route.path !== '/') {
|
|
||||||
return navigateTo('/')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const addConversation = (conversation) => {
|
export const addConversation = (conversation) => {
|
||||||
const conversations = useConversations()
|
const conversations = useConversations()
|
||||||
conversations.value = [conversation, ...conversations.value]
|
conversations.value = [conversation, ...conversations.value]
|
||||||
@@ -46,17 +33,12 @@ export const genTitle = async (conversationId) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (!error.value) {
|
if (!error.value) {
|
||||||
const route = useRoute()
|
|
||||||
const conversations = useConversations()
|
const conversations = useConversations()
|
||||||
const currentConversation = useConversation()
|
|
||||||
let index = conversations.value.findIndex(item => item.id === conversationId)
|
let index = conversations.value.findIndex(item => item.id === conversationId)
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
index = 0
|
index = 0
|
||||||
}
|
}
|
||||||
conversations.value[index].topic = data.value.title
|
conversations.value[index].topic = data.value.title
|
||||||
if (route.path === '/') {
|
|
||||||
currentConversation.value.topic = data.value.title
|
|
||||||
}
|
|
||||||
return data.value.title
|
return data.value.title
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|||||||
Reference in New Issue
Block a user