Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f3ab8c33b | ||
|
|
6522536291 | ||
|
|
2bca5a032c | ||
|
|
53460bd891 | ||
|
|
fb9e8b8c7d | ||
|
|
21dc2b9236 | ||
|
|
1a6bf1d239 | ||
|
|
3e3283029d | ||
|
|
16c9b0e230 | ||
|
|
836df995d0 | ||
|
|
5b9d52b177 | ||
|
|
deb627a9ab | ||
|
|
70efc09dae |
16
Dockerfile
16
Dockerfile
@@ -4,19 +4,23 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json yarn.lock ./
|
||||||
|
|
||||||
RUN yarn install
|
RUN yarn install && yarn cache clean
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN yarn generate
|
RUN yarn build
|
||||||
|
|
||||||
|
|
||||||
FROM nginx:alpine
|
FROM node:18-alpine3.16
|
||||||
|
|
||||||
|
ENV NITRO_PORT=80
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /app/.output/public .
|
COPY --from=builder /app/.output/ .
|
||||||
|
|
||||||
COPY nginx.conf /etc/nginx/templates/default.conf.template
|
|
||||||
|
|
||||||
EXPOSE 80
|
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"]
|
||||||
4
app.vue
4
app.vue
@@ -1,8 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
|
||||||
<NuxtLoadingIndicator />
|
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
|
<NuxtLoadingIndicator />
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
|
import {EventStreamContentType, fetchEventSource} from '@microsoft/fetch-event-source'
|
||||||
import {addConversation} from "../utils/helper";
|
|
||||||
|
|
||||||
const { $i18n, $auth } = useNuxtApp()
|
const { $i18n } = useNuxtApp()
|
||||||
const runtimeConfig = useRuntimeConfig()
|
const runtimeConfig = useRuntimeConfig()
|
||||||
const currentModel = useCurrentModel()
|
const currentModel = useCurrentModel()
|
||||||
const openaiApiKey = useApiKey()
|
const openaiApiKey = useApiKey()
|
||||||
@@ -108,12 +107,12 @@ const fetchReply = async (message) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event === 'done') {
|
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
|
props.conversation.id = data.conversationId
|
||||||
genTitle(props.conversation.id)
|
genTitle(props.conversation.id)
|
||||||
}
|
}
|
||||||
props.conversation.messages[props.conversation.messages.length - 1].id = data.messageId
|
|
||||||
abortFetch()
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,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) {
|
||||||
@@ -190,6 +183,7 @@ watchEffect(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div v-if="conversation">
|
||||||
<div
|
<div
|
||||||
v-if="conversation.loadingMessages"
|
v-if="conversation.loadingMessages"
|
||||||
class="text-center"
|
class="text-center"
|
||||||
@@ -201,7 +195,7 @@ watchEffect(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div
|
<div
|
||||||
v-if="conversation.messages.length > 0"
|
v-if="conversation.messages"
|
||||||
ref="chatWindow"
|
ref="chatWindow"
|
||||||
>
|
>
|
||||||
<v-container>
|
<v-container>
|
||||||
@@ -236,11 +230,14 @@ watchEffect(() => {
|
|||||||
|
|
||||||
<div ref="grab" class="w-100" style="height: 200px;"></div>
|
<div ref="grab" class="w-100" style="height: 200px;"></div>
|
||||||
</div>
|
</div>
|
||||||
<Welcome v-if="conversation.id === null && conversation.messages.length === 0" />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<v-footer app>
|
<v-footer
|
||||||
|
app
|
||||||
|
class="footer"
|
||||||
|
>
|
||||||
<div class="px-md-16 w-100 d-flex flex-column">
|
<div class="px-md-16 w-100 d-flex flex-column">
|
||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center">
|
||||||
<v-btn
|
<v-btn
|
||||||
@@ -287,3 +284,9 @@ watchEffect(() => {
|
|||||||
</v-snackbar>
|
</v-snackbar>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.footer {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -23,8 +23,10 @@ const contentHtml = ref('')
|
|||||||
|
|
||||||
const contentElm = ref(null)
|
const contentElm = ref(null)
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(async () => {
|
||||||
contentHtml.value = props.message.message ? md.render(props.message.message) : ''
|
contentHtml.value = props.message.message ? md.render(props.message.message) : ''
|
||||||
|
await nextTick()
|
||||||
|
bindCopyCodeToButtons()
|
||||||
})
|
})
|
||||||
|
|
||||||
const bindCopyCodeToButtons = () => {
|
const bindCopyCodeToButtons = () => {
|
||||||
@@ -49,10 +51,7 @@ const bindCopyCodeToButtons = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
bindCopyCodeToButtons()
|
console.log('mounted')
|
||||||
})
|
|
||||||
|
|
||||||
onUpdated(() => {
|
|
||||||
bindCopyCodeToButtons()
|
bindCopyCodeToButtons()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -64,18 +63,16 @@ onUpdated(() => {
|
|||||||
rounded="lg"
|
rounded="lg"
|
||||||
elevation="2"
|
elevation="2"
|
||||||
>
|
>
|
||||||
<v-card-text>
|
|
||||||
<div
|
<div
|
||||||
ref="contentElm"
|
ref="contentElm"
|
||||||
v-html="contentHtml"
|
v-html="contentHtml"
|
||||||
class="chat-msg-content"
|
class="chat-msg-content pa-3"
|
||||||
></div>
|
></div>
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.chat-msg-content ol {
|
.chat-msg-content ol, .chat-msg-content ul {
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
}
|
}
|
||||||
.hljs-code-container {
|
.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>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex-grow-1 d-flex align-center justify-space-between"
|
class="flex-grow-1 d-flex align-center justify-space-between"
|
||||||
@@ -25,67 +95,3 @@
|
|||||||
></v-btn>
|
></v-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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
|
menu.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted( () => {
|
onNuxtReady( () => {
|
||||||
loadPrompts()
|
loadPrompts()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,10 +4,9 @@
|
|||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h2 class="text-h2">{{ $t('welcomeTo') }} <span class="text-primary">{{ runtimeConfig.public.appName }}</span></h2>
|
<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') }}
|
{{ runtimeConfig.public.appName }} {{ $t('welcomeScreen.introduction1') }}
|
||||||
<br>
|
<br>
|
||||||
{{ $t('welcomeScreen.introduction2') }}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</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 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', () => {})
|
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
|
platform: linux/x86_64
|
||||||
build: .
|
build: .
|
||||||
environment:
|
environment:
|
||||||
SERVER_DOMAIN: http://web-server
|
SERVER_DOMAIN: ${SERVER_DOMAIN:-http://web-server}
|
||||||
|
NUXT_PUBLIC_TYPEWRITER: false
|
||||||
ports:
|
ports:
|
||||||
- '${CLIENT_PORT:-8080}:80'
|
- '${CLIENT_PORT:-80}:80'
|
||||||
networks:
|
networks:
|
||||||
- chatgpt_network
|
- chatgpt_network
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
chatgpt_network:
|
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,372 +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) {
|
|
||||||
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>
|
<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-list>
|
|
||||||
<v-list-item
|
|
||||||
:title="$auth.user.username"
|
|
||||||
:subtitle="$auth.user.email"
|
|
||||||
>
|
|
||||||
<template v-slot:prepend>
|
|
||||||
<v-icon
|
|
||||||
icon="face"
|
|
||||||
size="x-large"
|
|
||||||
></v-icon>
|
|
||||||
</template>
|
|
||||||
<template v-slot:append>
|
|
||||||
<v-menu>
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<v-btn
|
|
||||||
v-bind="props"
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
icon="expand_more"
|
|
||||||
></v-btn>
|
|
||||||
</template>
|
|
||||||
<v-list>
|
|
||||||
<v-list-item
|
|
||||||
:title="$t('resetPassword')"
|
|
||||||
to="/account/resetPassword"
|
|
||||||
>
|
|
||||||
</v-list-item>
|
|
||||||
<v-list-item
|
|
||||||
:title="$t('signOut')"
|
|
||||||
@click="signOut"
|
|
||||||
>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
|
|
||||||
<v-divider></v-divider>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="px-2">
|
|
||||||
<v-list>
|
|
||||||
<v-list-item v-show="loadingConversations">
|
|
||||||
<v-list-item-title class="d-flex justify-center">
|
|
||||||
<v-progress-circular indeterminate></v-progress-circular>
|
|
||||||
</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 : $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>{{ currentConversation.id ? 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>
|
|
||||||
|
|
||||||
</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>
|
|
||||||
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'
|
const appName = process.env.NUXT_PUBLIC_APP_NAME ?? 'ChatGPT UI'
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
dev: false,
|
debug: process.env.NODE_ENV !== 'production',
|
||||||
ssr: false,
|
ssr: true,
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
title: appName,
|
title: appName,
|
||||||
@@ -28,7 +28,7 @@ export default defineNuxtConfig({
|
|||||||
modules: [
|
modules: [
|
||||||
'@kevinmarrec/nuxt-pwa',
|
'@kevinmarrec/nuxt-pwa',
|
||||||
'@nuxtjs/color-mode',
|
'@nuxtjs/color-mode',
|
||||||
'@nuxtjs/i18n',
|
'@nuxtjs/i18n'
|
||||||
],
|
],
|
||||||
pwa: {
|
pwa: {
|
||||||
manifest: {
|
manifest: {
|
||||||
@@ -68,15 +68,5 @@ export default defineNuxtConfig({
|
|||||||
vueI18n: {
|
vueI18n: {
|
||||||
fallbackLocale: 'en',
|
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/color-mode": "^3.2.0",
|
||||||
"@nuxtjs/i18n": "^8.0.0-beta.9",
|
"@nuxtjs/i18n": "^8.0.0-beta.9",
|
||||||
"material-design-icons-iconfont": "^6.7.0",
|
"material-design-icons-iconfont": "^6.7.0",
|
||||||
"nuxt": "^3.2.0"
|
"nuxt": "^3.3.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@microsoft/fetch-event-source": "^2.0.1",
|
"@microsoft/fetch-event-source": "^2.0.1",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const route = useRoute()
|
|||||||
const sending = ref(false)
|
const sending = ref(false)
|
||||||
const resent = ref(false)
|
const resent = ref(false)
|
||||||
const errorMsg = ref(null)
|
const errorMsg = ref(null)
|
||||||
|
const user = useUser()
|
||||||
const resendEmail = async () => {
|
const resendEmail = async () => {
|
||||||
errorMsg.value = null
|
errorMsg.value = null
|
||||||
sending.value = true
|
sending.value = true
|
||||||
@@ -54,7 +55,7 @@ onNuxtReady(() => {
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<h2 class="text-h4">Verify your email</h2>
|
<h2 class="text-h4">Verify your email</h2>
|
||||||
<p class="mt-5">
|
<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.
|
Please check your inbox and click the link to verify your email address.
|
||||||
</p>
|
</p>
|
||||||
<p v-if="errorMsg"
|
<p v-if="errorMsg"
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ const fieldErrors = ref({
|
|||||||
new_password1: '',
|
new_password1: '',
|
||||||
new_password2: '',
|
new_password2: '',
|
||||||
})
|
})
|
||||||
const { $auth } = useNuxtApp()
|
|
||||||
const errorMsg = ref(null)
|
const errorMsg = ref(null)
|
||||||
const resetForm = ref(null)
|
const resetForm = ref(null)
|
||||||
const valid = ref(true)
|
const valid = ref(true)
|
||||||
@@ -37,7 +36,7 @@ const signOut = async () => {
|
|||||||
method: 'POST'
|
method: 'POST'
|
||||||
})
|
})
|
||||||
if (!error.value) {
|
if (!error.value) {
|
||||||
await $auth.logout()
|
await logout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import {useUser} from "~/composables/states";
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'vuetify-app'
|
layout: 'vuetify-app'
|
||||||
})
|
})
|
||||||
@@ -82,10 +84,8 @@ const formRules = ref({
|
|||||||
v => !!v || 'Password is required'
|
v => !!v || 'Password is required'
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
const { $auth } = useNuxtApp()
|
|
||||||
const errorMsg = ref(null)
|
const errorMsg = ref(null)
|
||||||
const signInForm = ref(null)
|
const signInForm = ref(null)
|
||||||
const valid = ref(true)
|
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const passwordInputType = ref('password')
|
const passwordInputType = ref('password')
|
||||||
@@ -99,6 +99,7 @@ const submit = async () => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(formData.value)
|
body: JSON.stringify(formData.value)
|
||||||
})
|
})
|
||||||
|
submitting.value = false
|
||||||
if (error.value) {
|
if (error.value) {
|
||||||
if (error.value.status === 400) {
|
if (error.value.status === 400) {
|
||||||
if (error.value.data.non_field_errors) {
|
if (error.value.data.non_field_errors) {
|
||||||
@@ -108,10 +109,10 @@ const submit = async () => {
|
|||||||
errorMsg.value = 'Something went wrong. Please try again.'
|
errorMsg.value = 'Something went wrong. Please try again.'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$auth.setUser(data.value.user)
|
setUser(data.value.user)
|
||||||
navigateTo(route.query.callback || '/')
|
const callback = route.query.callback ? decodeURIComponent(route.query.callback) : '/'
|
||||||
|
await navigateTo(callback)
|
||||||
}
|
}
|
||||||
submitting.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ definePageMeta({
|
|||||||
layout: 'vuetify-app'
|
layout: 'vuetify-app'
|
||||||
})
|
})
|
||||||
|
|
||||||
const { $auth } = useNuxtApp()
|
|
||||||
|
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
@@ -74,7 +72,7 @@ const submit = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$auth.setUser(data.value.user)
|
setUser(data.value.user)
|
||||||
navigateTo('/account/onboarding?email_verification_required='+data.value.email_verification_required)
|
navigateTo('/account/onboarding?email_verification_required='+data.value.email_verification_required)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,17 @@ 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 () => {
|
||||||
const { data, error } = await useAuthFetch('/api/chat/conversations/' + route.params.id)
|
const { data, error } = await useAuthFetch('/api/chat/conversations/' + route.params.id)
|
||||||
if (!error.value) {
|
if (!error.value) {
|
||||||
conversation.value = Object.assign(conversation.value, data.value)
|
conversation.value = Object.assign(conversation.value, data.value)
|
||||||
console.log(conversation.value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,24 +25,67 @@ const loadMessage = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onActivated(async () => {
|
const createNewConversation = () => {
|
||||||
console.log('activated')
|
if (route.path !== '/') {
|
||||||
|
return navigateTo('/?new')
|
||||||
|
}
|
||||||
|
conversation.value = Object.assign(getDefaultConversationData(), {
|
||||||
|
topic: $i18n.t('newConversation')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
} else {
|
|
||||||
conversation.value = getDefaultConversationData()
|
|
||||||
}
|
}
|
||||||
currentConversation.value = Object.assign({}, conversation.value)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
console.log('conversation.value', 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>
|
</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" />
|
||||||
<Conversation :conversation="conversation" />
|
<Conversation :conversation="conversation" />
|
||||||
|
</v-main>
|
||||||
</template>
|
</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()
|
const { $i18n } = useNuxtApp()
|
||||||
return {
|
return {
|
||||||
id: null,
|
id: null,
|
||||||
topic: $i18n.t('defaultConversationTitle'),
|
topic: null,
|
||||||
messages: [],
|
messages: [],
|
||||||
loadingMessages: false,
|
loadingMessages: false,
|
||||||
}
|
}
|
||||||
@@ -17,11 +17,6 @@ export const getConversations = async () => {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createNewConversation = () => {
|
|
||||||
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]
|
||||||
@@ -58,12 +53,27 @@ const transformData = (list) => {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadSettings = async () => {
|
export const getSystemSettings = async () => {
|
||||||
const settings = useSettings()
|
|
||||||
const { data, error } = await useAuthFetch('/api/chat/settings/', {
|
const { data, error } = await useAuthFetch('/api/chat/settings/', {
|
||||||
method: 'GET'
|
method: 'GET',
|
||||||
})
|
})
|
||||||
if (!error.value) {
|
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) => {
|
const get = (key) => {
|
||||||
|
if (process.server) return
|
||||||
let val = localStorage.getItem(key)
|
let val = localStorage.getItem(key)
|
||||||
if (val) {
|
if (val) {
|
||||||
val = JSON.parse(val)
|
val = JSON.parse(val)
|
||||||
@@ -9,6 +9,7 @@ const get = (key) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const set = (key, val) => {
|
const set = (key, val) => {
|
||||||
|
if (process.server) return
|
||||||
localStorage.setItem(key, JSON.stringify(val))
|
localStorage.setItem(key, JSON.stringify(val))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user