Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f3ab8c33b | ||
|
|
6522536291 | ||
|
|
2bca5a032c | ||
|
|
53460bd891 | ||
|
|
fb9e8b8c7d | ||
|
|
21dc2b9236 | ||
|
|
1a6bf1d239 | ||
|
|
3e3283029d | ||
|
|
16c9b0e230 | ||
|
|
836df995d0 | ||
|
|
5b9d52b177 | ||
|
|
deb627a9ab | ||
|
|
70efc09dae | ||
|
|
8ff914582a | ||
|
|
f20a3562f3 |
16
Dockerfile
16
Dockerfile
@@ -4,19 +4,23 @@ WORKDIR /app
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
RUN yarn install
|
||||
RUN yarn install && yarn cache clean
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN yarn generate
|
||||
RUN yarn build
|
||||
|
||||
|
||||
FROM nginx:alpine
|
||||
FROM node:18-alpine3.16
|
||||
|
||||
ENV NITRO_PORT=80
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/.output/public .
|
||||
|
||||
COPY nginx.conf /etc/nginx/templates/default.conf.template
|
||||
COPY --from=builder /app/.output/ .
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
# TODO: You can use NITRO_PRESET=node_cluster in order to leverage multi-process performance using Node.js cluster module. https://nuxt.com/docs/getting-started/deployment
|
||||
|
||||
ENTRYPOINT ["node", "server/index.mjs"]
|
||||
8
app.vue
8
app.vue
@@ -1,8 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<NuxtLayout>
|
||||
<NuxtLoadingIndicator />
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
@@ -1,8 +1,7 @@
|
||||
<script setup>
|
||||
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
|
||||
import {addConversation} from "../utils/helper";
|
||||
|
||||
const { $i18n, $auth } = useNuxtApp()
|
||||
const { $i18n } = useNuxtApp()
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const currentModel = useCurrentModel()
|
||||
const openaiApiKey = useApiKey()
|
||||
@@ -108,12 +107,12 @@ const fetchReply = async (message) => {
|
||||
}
|
||||
|
||||
if (event === 'done') {
|
||||
if (props.conversation.id === null) {
|
||||
abortFetch()
|
||||
props.conversation.messages[props.conversation.messages.length - 1].id = data.messageId
|
||||
if (!props.conversation.id) {
|
||||
props.conversation.id = data.conversationId
|
||||
genTitle(props.conversation.id)
|
||||
}
|
||||
props.conversation.messages[props.conversation.messages.length - 1].id = data.messageId
|
||||
abortFetch()
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -138,12 +137,6 @@ const scrollChatWindow = () => {
|
||||
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) {
|
||||
@@ -190,57 +183,61 @@ watchEffect(() => {
|
||||
</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">
|
||||
<div
|
||||
v-if="conversation.messages.length > 0"
|
||||
ref="chatWindow"
|
||||
v-if="conversation.loadingMessages"
|
||||
class="text-center"
|
||||
>
|
||||
<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>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
></v-progress-circular>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
v-if="conversation.messages"
|
||||
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>
|
||||
</div>
|
||||
<Welcome v-if="conversation.id === null && conversation.messages.length === 0" />
|
||||
</div>
|
||||
|
||||
|
||||
<v-footer app>
|
||||
<v-footer
|
||||
app
|
||||
class="footer"
|
||||
>
|
||||
<div class="px-md-16 w-100 d-flex flex-column">
|
||||
<div class="d-flex align-center">
|
||||
<v-btn
|
||||
@@ -287,3 +284,9 @@ watchEffect(() => {
|
||||
</v-snackbar>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.footer {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -23,8 +23,10 @@ const contentHtml = ref('')
|
||||
|
||||
const contentElm = ref(null)
|
||||
|
||||
watchEffect(() => {
|
||||
watchEffect(async () => {
|
||||
contentHtml.value = props.message.message ? md.render(props.message.message) : ''
|
||||
await nextTick()
|
||||
bindCopyCodeToButtons()
|
||||
})
|
||||
|
||||
const bindCopyCodeToButtons = () => {
|
||||
@@ -49,10 +51,7 @@ const bindCopyCodeToButtons = () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
bindCopyCodeToButtons()
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
console.log('mounted')
|
||||
bindCopyCodeToButtons()
|
||||
})
|
||||
|
||||
@@ -64,18 +63,16 @@ onUpdated(() => {
|
||||
rounded="lg"
|
||||
elevation="2"
|
||||
>
|
||||
<v-card-text>
|
||||
<div
|
||||
ref="contentElm"
|
||||
v-html="contentHtml"
|
||||
class="chat-msg-content"
|
||||
></div>
|
||||
</v-card-text>
|
||||
<div
|
||||
ref="contentElm"
|
||||
v-html="contentHtml"
|
||||
class="chat-msg-content pa-3"
|
||||
></div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.chat-msg-content ol {
|
||||
.chat-msg-content ol, .chat-msg-content ul {
|
||||
padding-left: 2em;
|
||||
}
|
||||
.hljs-code-container {
|
||||
|
||||
@@ -1,3 +1,73 @@
|
||||
<script setup>
|
||||
import { isMobile } from 'is-mobile'
|
||||
const { $i18n } = useNuxtApp()
|
||||
|
||||
const props = defineProps({
|
||||
sendMessage: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const message = ref('')
|
||||
const rows = ref(1)
|
||||
const autoGrow = ref(true)
|
||||
|
||||
const hint = computed(() => {
|
||||
return isMobile() ? '' : $i18n.t('pressEnterToSendYourMessageOrShiftEnterToAddANewLine')
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const lines = message.value.split(/\r\n|\r|\n/).length
|
||||
if (lines > 8) {
|
||||
rows.value = 8
|
||||
autoGrow.value = false
|
||||
} else {
|
||||
rows.value = 1
|
||||
autoGrow.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const send = () => {
|
||||
let msg = message.value
|
||||
// remove the last "\n"
|
||||
if (msg[msg.length - 1] === "\n") {
|
||||
msg = msg.slice(0, -1)
|
||||
}
|
||||
if (msg.length > 0) {
|
||||
props.sendMessage(msg)
|
||||
}
|
||||
message.value = ""
|
||||
}
|
||||
|
||||
const usePrompt = (prompt) => {
|
||||
message.value = prompt
|
||||
}
|
||||
|
||||
const clickSendBtn = () => {
|
||||
send()
|
||||
}
|
||||
|
||||
const enterOnly = (event) => {
|
||||
event.preventDefault();
|
||||
if (!isMobile()) {
|
||||
send()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
usePrompt
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex-grow-1 d-flex align-center justify-space-between"
|
||||
@@ -25,67 +95,3 @@
|
||||
></v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isMobile } from 'is-mobile'
|
||||
export default {
|
||||
name: "MsgEditor",
|
||||
props: {
|
||||
sendMessage: Function,
|
||||
disabled: Boolean,
|
||||
loading: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
message: "",
|
||||
rows: 1,
|
||||
autoGrow: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hint() {
|
||||
return isMobile() ? "" : "Press Enter to send your message or Shift+Enter to add a new line";
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
message(val) {
|
||||
const lines = val.split(/\r\n|\r|\n/).length;
|
||||
if (lines > 8) {
|
||||
this.rows = 8;
|
||||
this.autoGrow = false;
|
||||
} else {
|
||||
this.rows = 1;
|
||||
this.autoGrow = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
send() {
|
||||
let msg = this.message
|
||||
// remove the last "\n"
|
||||
if (msg[msg.length - 1] === "\n") {
|
||||
msg = msg.slice(0, -1)
|
||||
}
|
||||
if (msg.length > 0) {
|
||||
this.sendMessage(msg)
|
||||
}
|
||||
this.message = ""
|
||||
},
|
||||
usePrompt(prompt) {
|
||||
this.message = prompt
|
||||
},
|
||||
clickSendBtn () {
|
||||
this.send()
|
||||
},
|
||||
enterOnly (event) {
|
||||
event.preventDefault();
|
||||
if (!isMobile()) {
|
||||
this.send()
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
350
components/NavigationDrawer.vue
Normal file
350
components/NavigationDrawer.vue
Normal file
@@ -0,0 +1,350 @@
|
||||
<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"
|
||||
:title="$t('themeMode')"
|
||||
>
|
||||
<template
|
||||
v-slot:prepend
|
||||
>
|
||||
<v-icon
|
||||
v-show="$colorMode.value === 'light'"
|
||||
icon="light_mode"
|
||||
></v-icon>
|
||||
<v-icon
|
||||
v-show="$colorMode.value !== 'light'"
|
||||
icon="dark_mode"
|
||||
></v-icon>
|
||||
</template>
|
||||
</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>
|
||||
@@ -86,7 +86,7 @@ const selectPrompt = (prompt) => {
|
||||
menu.value = false
|
||||
}
|
||||
|
||||
onMounted( () => {
|
||||
onNuxtReady( () => {
|
||||
loadPrompts()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
<v-col cols="12">
|
||||
<div class="text-center">
|
||||
<h2 class="text-h2">{{ $t('welcomeTo') }} <span class="text-primary">{{ runtimeConfig.public.appName }}</span></h2>
|
||||
<p class="text-caption mt-5">
|
||||
<p class="text-caption my-5">
|
||||
{{ runtimeConfig.public.appName }} {{ $t('welcomeScreen.introduction1') }}
|
||||
<br>
|
||||
{{ $t('welcomeScreen.introduction2') }}
|
||||
</p>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
19
composables/fetch.js
Normal file
19
composables/fetch.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export const useMyFetch = (url, options = {}) => {
|
||||
let defaultOptions = {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
}
|
||||
if (process.server) {
|
||||
defaultOptions.baseURL = process.env.SERVER_DOMAIN
|
||||
}
|
||||
return useFetch(url, Object.assign(defaultOptions, options))
|
||||
}
|
||||
export const useAuthFetch = async (url, options = {}) => {
|
||||
const res = await useMyFetch(url, options)
|
||||
if (res.error.value && res.error.value.status === 401) {
|
||||
await logout()
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -5,8 +5,10 @@ export const useCurrentModel = () => useState('currentModel', () => getCurrentMo
|
||||
|
||||
export const useApiKey = () => useState('apiKey', () => getStoredApiKey())
|
||||
|
||||
export const useConversation = () => useState('conversation', () => getDefaultConversationData())
|
||||
|
||||
export const useConversations = () => useState('conversations', () => [])
|
||||
|
||||
export const useSettings = () => useState('settings', () => {})
|
||||
export const useSettings = () => useState('settings', () => getSystemSettings())
|
||||
|
||||
export const useUser = () => useState('user', () => null)
|
||||
|
||||
export const useDrawer = () => useState('drawer', () => false)
|
||||
@@ -1,9 +0,0 @@
|
||||
export const useAuthFetch = async (url, options = {}) => {
|
||||
const { $auth } = useNuxtApp()
|
||||
|
||||
const res = await useFetch(url, options)
|
||||
if (res.error.value && res.error.value.status === 401) {
|
||||
await $auth.logout()
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -4,13 +4,14 @@ services:
|
||||
platform: linux/x86_64
|
||||
build: .
|
||||
environment:
|
||||
SERVER_DOMAIN: http://web-server
|
||||
SERVER_DOMAIN: ${SERVER_DOMAIN:-http://web-server}
|
||||
NUXT_PUBLIC_TYPEWRITER: false
|
||||
ports:
|
||||
- '${CLIENT_PORT:-8080}:80'
|
||||
- '${CLIENT_PORT:-80}:80'
|
||||
networks:
|
||||
- chatgpt_network
|
||||
restart: always
|
||||
|
||||
networks:
|
||||
chatgpt_network:
|
||||
external: True
|
||||
driver: bridge
|
||||
|
||||
16
docker-compose.test.yml
Normal file
16
docker-compose.test.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
version: '3'
|
||||
services:
|
||||
client:
|
||||
platform: linux/x86_64
|
||||
build: .
|
||||
environment:
|
||||
SERVER_DOMAIN: ${SERVER_DOMAIN:-http://web-server}
|
||||
ports:
|
||||
- '${CLIENT_PORT:-80}:80'
|
||||
networks:
|
||||
- chatgpt_network
|
||||
restart: always
|
||||
|
||||
networks:
|
||||
chatgpt_network:
|
||||
driver: bridge
|
||||
@@ -1,368 +1,8 @@
|
||||
<script setup>
|
||||
import {useDisplay} from "vuetify";
|
||||
|
||||
const { $i18n, $auth } = 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) {
|
||||
if (conversations.value[index].id === currentConversation.value.id) {
|
||||
console.log('delete current conversation')
|
||||
createNewConversation()
|
||||
}
|
||||
conversations.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
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 $auth.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'
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
loadConversations()
|
||||
loadSettings()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app
|
||||
:theme="$colorMode.value"
|
||||
>
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
:permanent="drawerPermanent"
|
||||
width="300"
|
||||
>
|
||||
<template
|
||||
v-slot:prepend
|
||||
>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
prepend-icon="face"
|
||||
:title="$auth.user.username"
|
||||
:subtitle="$auth.user.email"
|
||||
>
|
||||
<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 }}</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>{{ currentConversation.topic ?? runtimeConfig.public.appName }}</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>
|
||||
|
||||
<NavigationDrawer />
|
||||
<slot />
|
||||
</v-app>
|
||||
</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>
|
||||
18
middleware/auth.ts
Normal file
18
middleware/auth.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
const user = useUser()
|
||||
const signInPath = '/account/signin'
|
||||
if (!user.value && to.path !== signInPath) {
|
||||
const { error, data} = await fetchUser()
|
||||
if (error.value) {
|
||||
return navigateTo({
|
||||
path: signInPath,
|
||||
query: {
|
||||
callback: encodeURIComponent(to.fullPath)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setUser(data.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -2,8 +2,8 @@
|
||||
const appName = process.env.NUXT_PUBLIC_APP_NAME ?? 'ChatGPT UI'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
dev: false,
|
||||
ssr: false,
|
||||
debug: process.env.NODE_ENV !== 'production',
|
||||
ssr: true,
|
||||
app: {
|
||||
head: {
|
||||
title: appName,
|
||||
@@ -28,7 +28,7 @@ export default defineNuxtConfig({
|
||||
modules: [
|
||||
'@kevinmarrec/nuxt-pwa',
|
||||
'@nuxtjs/color-mode',
|
||||
'@nuxtjs/i18n',
|
||||
'@nuxtjs/i18n'
|
||||
],
|
||||
pwa: {
|
||||
manifest: {
|
||||
@@ -68,15 +68,5 @@ export default defineNuxtConfig({
|
||||
vueI18n: {
|
||||
fallbackLocale: 'en',
|
||||
},
|
||||
},
|
||||
nitro: {
|
||||
devProxy: {
|
||||
"/api": {
|
||||
target: process.env.NUXT_DEV_SERVER ?? 'http://localhost:8000/api',
|
||||
prependPath: true,
|
||||
changeOrigin: true,
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"@nuxtjs/color-mode": "^3.2.0",
|
||||
"@nuxtjs/i18n": "^8.0.0-beta.9",
|
||||
"material-design-icons-iconfont": "^6.7.0",
|
||||
"nuxt": "^3.2.0"
|
||||
"nuxt": "^3.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
|
||||
@@ -7,6 +7,7 @@ const route = useRoute()
|
||||
const sending = ref(false)
|
||||
const resent = ref(false)
|
||||
const errorMsg = ref(null)
|
||||
const user = useUser()
|
||||
const resendEmail = async () => {
|
||||
errorMsg.value = null
|
||||
sending.value = true
|
||||
@@ -54,7 +55,7 @@ onNuxtReady(() => {
|
||||
<div v-else>
|
||||
<h2 class="text-h4">Verify your email</h2>
|
||||
<p class="mt-5">
|
||||
We've sent a verification email to <strong>{{ $auth.user.email }}</strong>. <br>
|
||||
We've sent a verification email to <strong>{{ user.email }}</strong>. <br>
|
||||
Please check your inbox and click the link to verify your email address.
|
||||
</p>
|
||||
<p v-if="errorMsg"
|
||||
|
||||
@@ -24,7 +24,6 @@ const fieldErrors = ref({
|
||||
new_password1: '',
|
||||
new_password2: '',
|
||||
})
|
||||
const { $auth } = useNuxtApp()
|
||||
const errorMsg = ref(null)
|
||||
const resetForm = ref(null)
|
||||
const valid = ref(true)
|
||||
@@ -37,7 +36,7 @@ const signOut = async () => {
|
||||
method: 'POST'
|
||||
})
|
||||
if (!error.value) {
|
||||
await $auth.logout()
|
||||
await logout()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useUser} from "~/composables/states";
|
||||
|
||||
definePageMeta({
|
||||
layout: 'vuetify-app'
|
||||
})
|
||||
@@ -82,10 +84,8 @@ const formRules = ref({
|
||||
v => !!v || 'Password is required'
|
||||
]
|
||||
})
|
||||
const { $auth } = useNuxtApp()
|
||||
const errorMsg = ref(null)
|
||||
const signInForm = ref(null)
|
||||
const valid = ref(true)
|
||||
const submitting = ref(false)
|
||||
const route = useRoute()
|
||||
const passwordInputType = ref('password')
|
||||
@@ -99,6 +99,7 @@ const submit = async () => {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData.value)
|
||||
})
|
||||
submitting.value = false
|
||||
if (error.value) {
|
||||
if (error.value.status === 400) {
|
||||
if (error.value.data.non_field_errors) {
|
||||
@@ -108,10 +109,10 @@ const submit = async () => {
|
||||
errorMsg.value = 'Something went wrong. Please try again.'
|
||||
}
|
||||
} else {
|
||||
$auth.setUser(data.value.user)
|
||||
navigateTo(route.query.callback || '/')
|
||||
setUser(data.value.user)
|
||||
const callback = route.query.callback ? decodeURIComponent(route.query.callback) : '/'
|
||||
await navigateTo(callback)
|
||||
}
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ definePageMeta({
|
||||
layout: 'vuetify-app'
|
||||
})
|
||||
|
||||
const { $auth } = useNuxtApp()
|
||||
|
||||
const formData = ref({
|
||||
username: '',
|
||||
email: '',
|
||||
@@ -74,7 +72,7 @@ const submit = async () => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$auth.setUser(data.value.user)
|
||||
setUser(data.value.user)
|
||||
navigateTo('/account/onboarding?email_verification_required='+data.value.email_verification_required)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,17 @@ definePageMeta({
|
||||
path: '/:id?',
|
||||
keepalive: true
|
||||
})
|
||||
|
||||
const { $i18n } = useNuxtApp()
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const drawer = useDrawer()
|
||||
const route = useRoute()
|
||||
const currentConversation = useConversation()
|
||||
const conversation = ref(getDefaultConversationData())
|
||||
|
||||
const loadConversation = async () => {
|
||||
const { data, error } = await useAuthFetch('/api/chat/conversations/' + route.params.id)
|
||||
if (!error.value) {
|
||||
conversation.value = data.value
|
||||
conversation.value = Object.assign(conversation.value, data.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,22 +25,67 @@ const loadMessage = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
onActivated(async () => {
|
||||
console.log('activated')
|
||||
console.log(conversation.value)
|
||||
const createNewConversation = () => {
|
||||
if (route.path !== '/') {
|
||||
return navigateTo('/?new')
|
||||
}
|
||||
conversation.value = Object.assign(getDefaultConversationData(), {
|
||||
topic: $i18n.t('newConversation')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
if (route.params.id) {
|
||||
conversation.value.loadingMessages = true
|
||||
await loadConversation()
|
||||
await loadMessage()
|
||||
conversation.value.loadingMessages = false
|
||||
} else {
|
||||
conversation.value = getDefaultConversationData()
|
||||
}
|
||||
console.log(conversation.value)
|
||||
currentConversation.value = Object.assign({}, conversation.value)
|
||||
})
|
||||
|
||||
|
||||
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 () => {
|
||||
if (route.path === '/' && route.query.new !== undefined) {
|
||||
createNewConversation()
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<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" />
|
||||
<Conversation :conversation="conversation" />
|
||||
</v-main>
|
||||
</template>
|
||||
@@ -1,71 +0,0 @@
|
||||
|
||||
const AUTH_ROUTE = {
|
||||
home: '/',
|
||||
login: '/account/signin',
|
||||
}
|
||||
|
||||
const ENDPOINTS = {
|
||||
login: {
|
||||
url: '/api/account/login/'
|
||||
},
|
||||
user: {
|
||||
url: '/api/account/user/'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
|
||||
class Auth {
|
||||
constructor() {
|
||||
this.loginIn = useState('loginIn', () => false)
|
||||
this.user = useState('user')
|
||||
}
|
||||
|
||||
async logout () {
|
||||
this.loginIn.value = false
|
||||
this.user.value = null
|
||||
await this.redirectToLogin()
|
||||
}
|
||||
|
||||
setUser (user) {
|
||||
this.user = user
|
||||
this.loginIn.value = true
|
||||
}
|
||||
|
||||
async fetchUser () {
|
||||
const { data, error } = await useFetch(ENDPOINTS.user.url, {
|
||||
// withCredentials: true
|
||||
})
|
||||
if (!error.value) {
|
||||
this.setUser(data.value)
|
||||
return null
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
async redirectToLogin (callback) {
|
||||
return await navigateTo(
|
||||
AUTH_ROUTE.login + '?callback=' + encodeURIComponent(callback || AUTH_ROUTE.home)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const auth = new Auth()
|
||||
|
||||
addRouteMiddleware('auth', async (to, from) => {
|
||||
if (!auth.loginIn.value) {
|
||||
const error = await auth.fetchUser()
|
||||
if (error) {
|
||||
return await auth.redirectToLogin(to.fullPath)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
provide: {
|
||||
auth
|
||||
}
|
||||
}
|
||||
})
|
||||
33
server/middleware/apiProxy.ts
Normal file
33
server/middleware/apiProxy.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
const PayloadMethods = new Set(["PATCH", "POST", "PUT", "DELETE"]);
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// @ts-ignore
|
||||
if (event.node.req.url.startsWith('/api/')) {
|
||||
// TODO: fix fetch failed
|
||||
const target = (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,
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
})
|
||||
@@ -3,7 +3,7 @@ export const getDefaultConversationData = () => {
|
||||
const { $i18n } = useNuxtApp()
|
||||
return {
|
||||
id: null,
|
||||
topic: $i18n.t('defaultConversationTitle'),
|
||||
topic: null,
|
||||
messages: [],
|
||||
loadingMessages: false,
|
||||
}
|
||||
@@ -17,13 +17,6 @@ export const getConversations = async () => {
|
||||
return []
|
||||
}
|
||||
|
||||
export const createNewConversation = () => {
|
||||
const conversation = useConversation()
|
||||
conversation.value = getDefaultConversationData()
|
||||
navigateTo('/')
|
||||
}
|
||||
|
||||
|
||||
export const addConversation = (conversation) => {
|
||||
const conversations = useConversations()
|
||||
conversations.value = [conversation, ...conversations.value]
|
||||
@@ -60,12 +53,27 @@ const transformData = (list) => {
|
||||
return result;
|
||||
}
|
||||
|
||||
export const loadSettings = async () => {
|
||||
const settings = useSettings()
|
||||
export const getSystemSettings = async () => {
|
||||
const { data, error } = await useAuthFetch('/api/chat/settings/', {
|
||||
method: 'GET'
|
||||
method: 'GET',
|
||||
})
|
||||
if (!error.value) {
|
||||
settings.value = transformData(data.value)
|
||||
return transformData(data.value)
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
export const fetchUser = async () => {
|
||||
return useMyFetch('/api/account/user/')
|
||||
}
|
||||
|
||||
export const setUser = (userData) => {
|
||||
const user = useUser()
|
||||
user.value = userData
|
||||
}
|
||||
|
||||
export const logout = () => {
|
||||
const user = useUser()
|
||||
user.value = null
|
||||
return navigateTo('/account/signin');
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import {MODELS} from "~/utils/enums";
|
||||
|
||||
const get = (key) => {
|
||||
if (process.server) return
|
||||
let val = localStorage.getItem(key)
|
||||
if (val) {
|
||||
val = JSON.parse(val)
|
||||
@@ -9,6 +9,7 @@ const get = (key) => {
|
||||
}
|
||||
|
||||
const set = (key, val) => {
|
||||
if (process.server) return
|
||||
localStorage.setItem(key, JSON.stringify(val))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user