api proxy

This commit is contained in:
Rafi
2023-04-05 23:17:14 +08:00
parent 21dc2b9236
commit fb9e8b8c7d
11 changed files with 431 additions and 439 deletions

View File

@@ -1,8 +1,6 @@
<template> <template>
<div>
<NuxtLoadingIndicator />
<NuxtLayout> <NuxtLayout>
<NuxtLoadingIndicator />
<NuxtPage /> <NuxtPage />
</NuxtLayout> </NuxtLayout>
</div>
</template> </template>

View File

@@ -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) {

View 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>

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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:

View File

@@ -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>

View File

@@ -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,
// }
//
// }
// },
}) })

View File

@@ -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,9 +25,15 @@ 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) {
@@ -32,21 +41,51 @@ onMounted(async () => {
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>

View File

@@ -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,
},
});
} }
}) })

View File

@@ -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